Compare commits

...
26 Commits
Author SHA1 Message Date
Lucas.Xu f3d6c66f5c chore: bump version 0.2.4 (#2890) 2023-06-23 15:36:44 +08:00
Lucas.Xu d665153a9f fix: 0.2.3 known issues (#2886)
* fix: option menu position

* fix: rename helps highlight the text for the user

* fix: export as markdown file name invalid

* chore: align the emoji in callout

* fix: leave more space in the editor

* fix: 0.2.3 known issues

* chore: add flutter pub get in flutter.toml
2023-06-23 10:54:35 +08:00
Askarbek Zadaulyandqinluhe eee32110f4 feat: nested views
* chore: remove folder code merge page and folder into navitem component

* chore: test fix

* fix: nav item expand fix

* fix: unfold page and active page

* fix: nav item click area fix

* chore: remove old components

* chore: remove old code

* chore: cell controller reorganize

* chore: nav item optimizations

* fix: add async queue to fix data problem

* chore: change semantics of new folder button

* chore: move row methods to database controller

---------

Co-authored-by: qinluhe <qinluhe.twodog@gmail.com>
2023-06-23 10:17:50 +08:00
Richard Shiue 9834eccc7b fix: remove icon button not working in popover (#2885) 2023-06-23 10:01:06 +08:00
Richard Shiue bef7fe87aa test: add more calendar integration tests (#2883)
* test: make sure that the row details page opens

* test: add more calendar tests
2023-06-22 23:40:49 +08:00
Lucas.Xu feceb430cf chore: optimize CI (#2875) 2023-06-22 21:22:46 +08:00
Nathan.foooandvedon a29c8ab27a fix: cell focus issue on windows (#2880)
* fix: cell focus issue on windows

* fix: try to fix

* fix: cell focus not working on Windows platform there are multiple text cells

* docs: add documentation

* chore: adjust row detail page ui

* test: add test

* test: fix test

---------

Co-authored-by: vedon <vedon.fu@gmail.com>
2023-06-22 20:16:31 +08:00
Nathan.foooandvedon f1bfcb6066 feat: only allow one windows instance (#2879)
Co-authored-by: vedon <vedon.fu@gmail.com>
2023-06-22 13:25:01 +08:00
Richard Shiue 99cfda0a5c test: more integration tests (#2871)
* test: date cell integration tests

* test: single/multi-select integration tests
2023-06-21 22:53:49 +08:00
Lucas.Xu 9bd629aaef fix: undo redo for the transforming block will raise an error (#2869)
* fix: undo redo for the transforming block will raise an error

* test: add golden for editing document

* test: add undo redo test
2023-06-21 19:53:29 +08:00
Nathan.fooo d302f6b3fb chore: adjust ui (#2872) 2023-06-21 19:47:30 +08:00
Nathan.foooandLucas.Xu 5595649f53 chore: add database sort integration test (#2866)
* chore: add sort test

* chore: add tests

* chore: update tests

* fix: switch folder test

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-21 19:10:16 +08:00
Nathan.fooo 7de17ab0b8 chore: bump version 0.2.3 (#2864)
* chore: bump version 0.2.3

* chore: update version in pubspec
2023-06-21 15:13:36 +08:00
Nathan.fooo 13ff38756f feat: put all user data into a folder when choosing a custom storage path (#2861) 2023-06-21 12:15:32 +08:00
Lucas.Xu 197e5b3a79 feat: upgrade editor version to latest 0.1.4 2023-06-21 11:36:50 +08:00
Alex Wallen 5cced92646 [feat] make error widget sexy (#2825)
* feat: add translations

* feat: style error page

* chore: update api usage

* feat: add GitHub redirect

* chore: rebase error?

* chore: remove todo comment

* chore: remove impossible code

* fix: update url launcher invocation

* chore: use new api

* fix: analyzer errors

* fix: revert changes in Cargo.toml
2023-06-21 10:03:39 +08:00
Nathan.fooo e50d708c21 feat: create database view on same database (#2829)
* feat: create database view on same database

* feat: switch tag between views

* fix: calendar tool bar

* fix: set layout setting

* chore: update collab rev

* fix: board layout issue

* test: add integration tests

* test: add calendar start from day test
2023-06-20 23:48:34 +08:00
Nathan.fooo 79fc7c4cfe fix: import markdown file (#2841) 2023-06-19 21:20:53 +08:00
Kilu.He 202cd90711 fix: build tauri app failed in macOS (#2828) 2023-06-17 22:02:03 +08:00
Kilu.He 177f7c4fa3 Support document multiple instance and change doc_id from String to &str (#2808)
* fix: support multiple document

* fix: change the doc_id params to ref

* fix: function to converge subscription state

* fix: mousedown behavior update

* fix: turn to textblock when the enter pressed in empty block

* fix: support cut

* fix: emoji caret

* fix: support slash arrow key

* fix: eslint padding-line-between-statements

* fix: add comment

* fix: block side menu bugs

* fix: support key event to select block menu option

* fix: support side menu arrow key

* fix: optimizate selectOptionByUpDown

* fix: format
2023-06-17 14:25:30 +08:00
Lucas.Xu d986e01d3e chore: use retry action v1.0.36 2023-06-16 20:28:31 +08:00
Daniil Golubev 8a8915c662 chore: update russian localization (#2787)
* feat(locale): add missing lines

* feat(locale): update russian translations
2023-06-16 15:33:48 +08:00
efc857d752 chore: some ui improvements (#2791)
* chore: some ui improvements

* fix: integration test

* feat: language selector on welcome page (#2796)

* feat: add language selector on welcome page

* feat: add hover effect and refactor layout

* test: add basic languge selector testing

* chore: increate place holder width

* fix: add catch error for setLocale and finish the testing

* chore: update comment

* feat: refactor the skip login in page and add tests

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>

* feat: row document (#2792)

* chore: create orphan view handler

* feat: save icon url and cover url in view

* feat: implement emoji picker UI

* chore: config ui

* chore: config ui again

* chore: replace RowPB with RowMetaPB to exposing more row information

* fix: compile error

* feat: show emoji in row

* chore: update

* test: insert emoji test

* test: add update emoji test

* test: add remove emoji test

* test: add create field tests

* test: add create row and delete row integration tests

* test: add create row from row menu

* test: document in row detail page

* test: delete, duplicate row in row detail page

* test: check the row count displayed in grid page

* test: rename existing field in grid page

* test: update field type of exisiting field in grid page

* test: delete field test

* test: add duplicate field test

* test: add hide field test

* test: add edit text cell test

* test: add insert text to text cell test

* test: add edit number cell test

* test: add edit multiple number cells

* test: add edit checkbox cell test

* feat: integrate editor into database row

* test: add edit create time and last edit time cell test

* test: add edit date cell by selecting a date test

* chore: remove unused code

* chore: update checklist bg color

* test: add update database layout test

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>

* test: fix test

* test: add create select option test

---------

Co-authored-by: Yijing Huang <hyj891204@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
Co-authored-by: nathan <nathan@appflowy.io>
2023-06-16 15:32:28 +08:00
Nathan.fooo 14dee6b797 test: filter integration (#2821)
* test: add checklist filter test

* fix: widget reference to invalid databaseController

* fix: SelectOptionFilterList doesn't expand to fill the CustomScrollView

* test: add single select and multi-select filter test

* ci: set protoc version
2023-06-16 14:32:32 +08:00
Lucas.Xu 63eee8b415 chore: retry codecov 2023-06-16 13:32:24 +08:00
Nathan.fooo d96a1d8bd4 test: import database integration (#2803)
* feat: support importing database raw data

* feat: verify import database test

* test: fix test

* ci: update integration test ci config

* ci: codecov with os flag

* ci: update docker command

* ci: update docker command

* ci: update docker command

* ci: update docker command

* test: add filter test
2023-06-15 22:43:07 +08:00
302 changed files with 8596 additions and 3625 deletions

No files matched your search

+4 -3
View File
@@ -21,12 +21,13 @@ on:
- unlocked
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-app:
if: github.event.pull_request.draft != true
concurrency:
group: docker_ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- name: Checkout source code
+17 -5
View File
@@ -25,8 +25,13 @@ env:
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
@@ -119,9 +124,16 @@ jobs:
run: |
cargo make dart_unit_test
- uses: codecov/codecov-action@v3
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with:
name: appflowy
flags: appflowy_flutter_unit_test
fail_ci_if_error: true
verbose: true
action: codecov/codecov-action@v3
with: |
name: appflowy
flags: appflowy_flutter_unit_test
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
attempt_limit: 5
attempt_delay: 10000
+44 -33
View File
@@ -6,7 +6,7 @@ on:
- "main"
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
- ".github/workflows/integration_test.yml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
@@ -15,15 +15,22 @@ on:
- "main"
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
- ".github/workflows/integration_test.yml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tests:
if: github.event.pull_request.draft != true
strategy:
matrix:
os: [ubuntu-latest]
@@ -36,31 +43,35 @@ jobs:
with:
toolchain: "stable-2022-04-07"
- uses: subosito/flutter-action@v2
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.10.1"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Cache Cargo
uses: actions/cache@v2
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
prefix-key: ${{ matrix.os }}
workspaces: |
frontend/rust-lib
- name: Cache Rust
id: cache-rust-target
uses: actions/cache@v2
- uses: davidB/rust-cargo-make@v1
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') }}
version: '0.36.6'
- name: Setup Environment
- name: Install prerequisites
working-directory: frontend
run: |
cargo install --force cargo-make
cargo install --force duckscript_cli
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
@@ -73,14 +84,8 @@ jobs:
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-flutter-deps-tools
shell: bash
- name: Config Flutter
run: |
@@ -112,17 +117,23 @@ jobs:
export DISPLAY=:99
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test integration_test/runner.dart -d Linux --coverage --verbose
flutter test integration_test/runner.dart -d Linux --coverage
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter test integration_test/runner.dart -d macOS --coverage --verbose
flutter test integration_test/runner.dart -d macOS --coverage
elif [ "$RUNNER_OS" == "Windows" ]; then
flutter test integration_test/runner.dart -d Windows --coverage --verbose
flutter test integration_test/runner.dart -d Windows --coverage
fi
shell: bash
- uses: codecov/codecov-action@v3
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with:
name: appflowy
flags: appflowy_flutter_integrateion_test
fail_ci_if_error: true
verbose: true
action: codecov/codecov-action@v3
with: |
name: appflowy
flags: appflowy_flutter_integrateion_test
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
attempt_limit: 5
attempt_delay: 10000
+7 -1
View File
@@ -2,6 +2,7 @@ name: Tauri-CI
on:
pull_request:
paths:
- ".github/workflows/tauri_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_tauri/**"
@@ -10,8 +11,13 @@ env:
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tauri-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
@@ -38,7 +44,7 @@ jobs:
with:
prefix-key: 'ubuntu-latest-tauri'
workspaces: |
frontend/rust-lib
frontend/rust-lib
frontend/appflowy_tauri/src-tauri
- name: install dependencies (windows only)
+31 -1
View File
@@ -1,24 +1,44 @@
# Release Notes
## Version 0.2.2 - 06/15/2023
## Version 0.2.4 - 06/23/2023
### Bug fixes:
- Unable to copy and paste a word
- Some UI issues
## Version 0.2.3 - 06/21/2023
### New Features
- Added support for creating multiple database views for existing database
## Version 0.2.2 - 06/15/2023
### New Features
- Added support for embedding a document in the database's row detail page
- Added support for inserting an emoji in the database's row detail page
### Other Updates
- Added language selector on the welcome page
- Added support for importing multiple markdown files all at once
## Version 0.2.1 - 06/11/2023
### New Features
- Added support for creating or referencing a calendar in the document
- Added `+` icon in grid's add field
### Other Updates
- Added vertical padding for progress bar
- Hide url cell accessory when the content is empty
### Bug fixes:
- Fixed unable to export markdown
- Fixed adding vertical padding for progress bar
- Fixed database view didn't update after the database layout changed.
@@ -26,6 +46,7 @@
## Version 0.2.0 - 06/08/2023
### New Features
- Improved checklists to support each cell having its own list
- Drag and drop calendar events
- Switch layouts (calendar, grid, kanban) of a database
@@ -39,44 +60,53 @@
- Added support for an 'Option' button to delete, duplicate, and customize block actions
### Other Updates
- Added support for importing v0.1.x documents and databases
- Added support for database import and export to CSV
- Optimized scroll behavior in documents.
- Redesigned the launch page
### Bug fixes
- Fixed bugs related to numbers
- Fixed issues with referenced databases in documents
- Fixed menu overflow issues in documents
### Data migration
The data format of this version is not compatible with previous versions. Therefore, to migrate your data to the new version, you need to use the export and import functions. Please follow the guide to learn how to export and import your data.
#### Export files in v0.1.6
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9
#### Import files in v0.2.0
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2
## Version 0.1.5 - 11/05/2023
### Bug Fixes
- Fix: calendar dates don't match with weekdays.
- Fix: sort numbers in Grid.
## Version 0.1.4 - 04/05/2023
### New features
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
- Writing can be improved with the help of OpenAI.
## Version 0.1.3 - 24/04/2023
### New features
- Launch the official Dark Mode.
- Customize the font color and highlight color by setting a hex color value and an opacity level.
### Bug Fixes
- Fix: the slash menu can be triggered by all other keyboards than English.
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
+1 -1
View File
@@ -23,7 +23,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.2.2"
CURRENT_APP_VERSION = "0.2.4"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
+2
View File
@@ -72,3 +72,5 @@ windows/flutter/dart_ffi/
*.env
coverage/
**/failures/*.png
@@ -0,0 +1,11 @@
"{""id"":""2_OVWb"",""name"":""Name"",""field_type"":0,""visibility"":true,""width"":150,""type_options"":{""0"":{""data"":""""}},""is_primary"":true}","{""id"":""xjmOSi"",""name"":""Type"",""field_type"":3,""visibility"":true,""width"":150,""type_options"":{""3"":{""content"":""{\""options\"":[{\""id\"":\""t1WZ\"",\""name\"":\""s6\"",\""color\"":\""Lime\""},{\""id\"":\""GzNa\"",\""name\"":\""s5\"",\""color\"":\""Yellow\""},{\""id\"":\""l_8w\"",\""name\"":\""s4\"",\""color\"":\""Orange\""},{\""id\"":\""TzVT\"",\""name\"":\""s3\"",\""color\"":\""LightPink\""},{\""id\"":\""b5WF\"",\""name\"":\""s2\"",\""color\"":\""Pink\""},{\""id\"":\""AcHA\"",\""name\"":\""s1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""Hpbiwr"",""name"":""Done"",""field_type"":5,""visibility"":true,""width"":150,""type_options"":{""5"":{""is_selected"":false}},""is_primary"":false}","{""id"":""F7WLnw"",""name"":""checklist"",""field_type"":7,""visibility"":true,""width"":120,""type_options"":{""0"":{""data"":""""},""7"":{}},""is_primary"":false}","{""id"":""KABhMe"",""name"":""number"",""field_type"":1,""visibility"":true,""width"":120,""type_options"":{""1"":{""format"":0,""symbol"":""RUB"",""scale"":0,""name"":""Number""},""0"":{""scale"":0,""data"":"""",""format"":0,""name"":""Number"",""symbol"":""RUB""}},""is_primary"":false}","{""id"":""lEn6Bv"",""name"":""date"",""field_type"":2,""visibility"":true,""width"":120,""type_options"":{""2"":{""field_type"":2,""time_format"":1,""timezone_id"":"""",""date_format"":3},""0"":{""field_type"":2,""date_format"":3,""time_format"":1,""data"":"""",""timezone_id"":""""}},""is_primary"":false}","{""id"":""B8Prnx"",""name"":""url"",""field_type"":6,""visibility"":true,""width"":120,""type_options"":{""6"":{""content"":"""",""url"":""""},""0"":{""content"":"""",""data"":"""",""url"":""""}},""is_primary"":false}","{""id"":""MwUow4"",""name"":""multi-select"",""field_type"":4,""visibility"":true,""width"":240,""type_options"":{""0"":{""content"":""{\""options\"":[],\""disable_color\"":false}"",""data"":""""},""4"":{""content"":""{\""options\"":[{\""id\"":\""__Us\"",\""name\"":\""m7\"",\""color\"":\""Green\""},{\""id\"":\""n9-g\"",\""name\"":\""m6\"",\""color\"":\""Lime\""},{\""id\"":\""KFYu\"",\""name\"":\""m5\"",\""color\"":\""Yellow\""},{\""id\"":\""KftP\"",\""name\"":\""m4\"",\""color\"":\""Orange\""},{\""id\"":\""5lWo\"",\""name\"":\""m3\"",\""color\"":\""LightPink\""},{\""id\"":\""Djrz\"",\""name\"":\""m2\"",\""color\"":\""Pink\""},{\""id\"":\""2uRu\"",\""name\"":\""m1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}"
"{""field_type"":0,""created_at"":1686793246,""data"":""A"",""last_modified"":1686793246}","{""last_modified"":1686793275,""created_at"":1686793261,""data"":""AcHA"",""field_type"":3}","{""created_at"":1686793241,""field_type"":5,""last_modified"":1686793241,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""pi1A\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""6Pym\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""erEe\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""pi1A\"",\""6Pym\""]}"",""created_at"":1686793302,""field_type"":7,""last_modified"":1686793308}","{""created_at"":1686793333,""field_type"":1,""data"":""-1"",""last_modified"":1686793333}","{""last_modified"":1686793370,""field_type"":2,""data"":""1685583770"",""include_time"":false,""created_at"":1686793370}","{""created_at"":1686793395,""data"":""appflowy.io"",""field_type"":6,""last_modified"":1686793399,""url"":""https://appflowy.io""}","{""last_modified"":1686793446,""field_type"":4,""data"":""2uRu"",""created_at"":1686793428}"
"{""last_modified"":1686793247,""data"":""B"",""field_type"":0,""created_at"":1686793247}","{""created_at"":1686793278,""data"":""b5WF"",""field_type"":3,""last_modified"":1686793278}","{""created_at"":1686793292,""last_modified"":1686793292,""data"":""Yes"",""field_type"":5}","{""data"":""{\""options\"":[{\""id\"":\""YHDO\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""QjtW\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""K2nM\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""YHDO\""]}"",""field_type"":7,""last_modified"":1686793318,""created_at"":1686793311}","{""data"":""-2"",""last_modified"":1686793335,""created_at"":1686793335,""field_type"":1}","{""field_type"":2,""data"":""1685670174"",""include_time"":false,""created_at"":1686793374,""last_modified"":1686793374}","{""last_modified"":1686793403,""field_type"":6,""created_at"":1686793399,""url"":"""",""data"":""no url""}","{""data"":""2uRu,Djrz"",""field_type"":4,""last_modified"":1686793449,""created_at"":1686793449}"
"{""data"":""C"",""created_at"":1686793248,""last_modified"":1686793248,""field_type"":0}","{""created_at"":1686793280,""field_type"":3,""data"":""TzVT"",""last_modified"":1686793280}","{""data"":""Yes"",""last_modified"":1686793292,""field_type"":5,""created_at"":1686793292}","{""last_modified"":1686793329,""field_type"":7,""created_at"":1686793322,""data"":""{\""options\"":[{\""id\"":\""iWM1\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""WDvF\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""w3k7\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""iWM1\"",\""WDvF\"",\""w3k7\""]}""}","{""field_type"":1,""last_modified"":1686793339,""data"":""0.1"",""created_at"":1686793339}","{""last_modified"":1686793377,""data"":""1685756577"",""created_at"":1686793377,""include_time"":false,""field_type"":2}","{""created_at"":1686793403,""field_type"":6,""data"":""appflowy.io"",""last_modified"":1686793408,""url"":""https://appflowy.io""}","{""data"":""2uRu,Djrz,5lWo"",""created_at"":1686793453,""last_modified"":1686793454,""field_type"":4}"
"{""data"":""D"",""last_modified"":1686793249,""created_at"":1686793249,""field_type"":0}","{""data"":""l_8w"",""created_at"":1686793284,""last_modified"":1686793284,""field_type"":3}","{""data"":""Yes"",""created_at"":1686793293,""last_modified"":1686793293,""field_type"":5}",,"{""field_type"":1,""last_modified"":1686793341,""created_at"":1686793341,""data"":""0.2""}","{""created_at"":1686793379,""last_modified"":1686793379,""field_type"":2,""data"":""1685842979"",""include_time"":false}","{""last_modified"":1686793419,""field_type"":6,""created_at"":1686793408,""data"":""https://github.com/AppFlowy-IO/"",""url"":""https://github.com/AppFlowy-IO/""}","{""data"":""2uRu,Djrz,5lWo"",""last_modified"":1686793459,""field_type"":4,""created_at"":1686793459}"
"{""field_type"":0,""last_modified"":1686793250,""created_at"":1686793250,""data"":""E""}","{""field_type"":3,""last_modified"":1686793290,""created_at"":1686793290,""data"":""GzNa""}","{""last_modified"":1686793294,""created_at"":1686793294,""data"":""Yes"",""field_type"":5}",,"{""created_at"":1686793346,""field_type"":1,""last_modified"":1686793346,""data"":""1""}","{""last_modified"":1686793383,""data"":""1685929383"",""field_type"":2,""include_time"":false,""created_at"":1686793383}","{""field_type"":6,""url"":"""",""data"":"""",""last_modified"":1686793421,""created_at"":1686793419}","{""field_type"":4,""last_modified"":1686793465,""data"":""2uRu,Djrz,5lWo,KFYu,KftP"",""created_at"":1686793463}"
"{""field_type"":0,""created_at"":1686793251,""data"":"""",""last_modified"":1686793289}",,,,"{""data"":""2"",""field_type"":1,""created_at"":1686793347,""last_modified"":1686793347}","{""include_time"":false,""data"":""1685929385"",""last_modified"":1686793385,""field_type"":2,""created_at"":1686793385}",,
"{""created_at"":1686793254,""field_type"":0,""last_modified"":1686793288,""data"":""""}",,,,"{""created_at"":1686793351,""last_modified"":1686793351,""data"":""10"",""field_type"":1}","{""include_time"":false,""data"":""1686879792"",""field_type"":2,""created_at"":1686793392,""last_modified"":1686793392}",,
,,,,"{""last_modified"":1686793354,""created_at"":1686793354,""field_type"":1,""data"":""11""}",,,
,,,,"{""field_type"":1,""last_modified"":1686793356,""data"":""12"",""created_at"":1686793356}",,,
,,,,,,,
@@ -55,7 +55,8 @@
"textAndMarkdown": "Text & Markdown",
"documentFromV010": "Document from v0.1.0",
"databaseFromV010": "Database from v0.1.0",
"csv": "CSV"
"csv": "CSV",
"database": "Database"
},
"disclosureAction": {
"rename": "Rename",
@@ -222,6 +223,7 @@
"changeLocationTooltips": "Change the data directory",
"change": "Change",
"openLocationTooltips": "Open another data directory",
"openCurrentDataFolder": "Open current data directory",
"recoverLocationTooltips": "Reset to AppFlowy's default data directory",
"exportFileSuccess": "Export file successfully!",
"exportFileFail": "Export file failed!",
@@ -235,6 +237,8 @@
}
},
"grid": {
"deleteView": "Are you sure you want to delete this view?",
"createView": "New",
"settings": {
"filter": "Filter",
"sort": "Sort",
@@ -474,5 +478,10 @@
"name": "Calendar layout"
},
"referencedCalendarPrefix": "View of"
},
"errorDialog": {
"title": "AppFlowy Error",
"howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.",
"github": "View on GitHub"
}
}
}
@@ -45,7 +45,14 @@
"medium": "средний",
"large": "большой",
"fontSize": "Размер шрифта",
"import": "Импортировать"
"import": "Импортировать",
"moreOptions": ""
},
"importPanel": {
"textAndMarkdown": "Текст и Markdown",
"documentFromV010": "Документ из v0.1.0",
"databaseFromV010": "База данных из v0.1.0",
"csv": "CSV"
},
"disclosureAction": {
"rename": "Переименовать",
@@ -108,8 +115,9 @@
"openAsPage": "Открыть как страницу",
"addNewRow": "Добавить новую строку",
"openMenu": "Открыть меню",
"viewDataBase": "Просмотр базы данных",
"referencePage": "Ссылаются на {name}"
"dragRow": "Долгое нажатие для изменения порядка строк",
"viewDataBase": "Открыть базу данных",
"referencePage": "Ссылки на {name}"
},
"sideBar": {
"closeSidebar": "Закрыть боковое меню",
@@ -123,7 +131,7 @@
},
"contactsPage": {
"title": "Контакты",
"whatsHappening": "Какие события на этой неделе?",
"whatsHappening": "Что нового на этой неделе?",
"addContact": "Добавить контакт",
"editContact": "Редактировать контакт"
},
@@ -182,15 +190,20 @@
"theme": "Тема"
},
"files": {
"copy": "Копировать",
"defaultLocation": "Где сейчас хранятся ваши данные",
"exportData": "Экспорт данных",
"doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь",
"restoreLocation": "Восстановить путь AppFlowy по умолчанию",
"customizeLocation": "Открыть другую папку",
"restartApp": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.",
"exportDatabase": "Экспорт базы данных",
"selectFiles": "Выбрать файлы, которые необходимо экспортировать",
"selectAll": "Выбрать всё",
"deselectAll": "Снять выделение",
"createNewFolder": "Создать новую папку",
"createNewFolderDesc": "Указать, где хранить свои данные ...",
"defineWhereYourDataIsStored": "Указать хранилище данных",
"open": "Открыть",
"openFolder": "Открыть существующую папку",
"openFolderDesc": "Чтение и запись в существующую папку AppFlowy ...",
@@ -199,9 +212,17 @@
"locationDesc": "Выбрать имя папки данных AppFlowy",
"browser": "Обзор",
"create": "Создать",
"set": "Set",
"folderPath": "Путь к вашей папке",
"locationCannotBeEmpty": "Путь не может быть пустым",
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!"
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!",
"changeLocationTooltips": "Сменить местоположения",
"change": "Изменить",
"openLocationTooltips": "Открыть другую директорию данных",
"recoverLocationTooltips": "Сбросить к местоположению по умолчанию",
"exportFileSuccess": "Экспорт завершён!",
"exportFileFail": "Не удалость экспортировать!",
"export": "Экспорт"
},
"user": {
"name": "Имя",
@@ -221,7 +242,8 @@
"deleteFilter": "Удалить фильтр",
"filterBy": "Фильтровать по...",
"typeAValue": "Введите значение...",
"layout": "Раскладка"
"layout": "Вид",
"databaseLayout": "Вид базы данных"
},
"textFilter": {
"contains": "Содержит",
@@ -272,6 +294,8 @@
"textFieldName": "Текст",
"checkboxFieldName": "Чекбокс",
"dateFieldName": "Дата",
"updatedAtFieldName": "Последнее изменение",
"createdAtFieldName": "Создано",
"numberFieldName": "Число",
"singleSelectFieldName": "Выбор",
"multiSelectFieldName": "Выбор нескольких",
@@ -341,12 +365,16 @@
},
"slashMenu": {
"board": {
"selectABoardToLinkTo": "Выбрать доску для связи с",
"selectABoardToLinkTo": "Выбрать доску",
"createANewBoard": "Создать доску"
},
"grid": {
"selectAGridToLinkTo": "Выберите сетку для связи с",
"selectAGridToLinkTo": "Выберите сетку",
"createANewGrid": "Создать сетку"
},
"calendar": {
"selectACalendarToLinkTo": "Выбрать календарь",
"createANewCalendar": "Создать календарь"
}
},
"plugins": {
@@ -363,6 +391,8 @@
"smartEditFixSpelling": "Исправить правописание",
"warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.",
"smartEditSummarize": "Выделить суть",
"smartEditImproveWriting": "Улучшить",
"smartEditMakeLonger": "Продолжить",
"smartEditCouldNotFetchResult": "Не могу получить ответ от OpenAI",
"smartEditCouldNotFetchKey": "Не могу получить токен OpenAI",
"smartEditDisabled": "Подключить OpenAI",
@@ -394,6 +424,21 @@
"mathEquation": {
"addMathEquation": "Добавить математическое выражение",
"editMathEquation": "Редактировать математическое выражение"
},
"optionAction": {
"click": "Кликните",
"toOpenMenu": " чтобы открыть меню",
"delete": "Удалить",
"duplicate": "Дублировать",
"turnInto": "Превратить",
"moveUp": "Поднять",
"moveDown": "Опустить",
"color": "Цвет",
"align": "Выравнивание",
"left": "По левому краю",
"center": "По центру",
"right": "По правому краю",
"defaultColor": "Цвет по умолчанию"
}
}
},
@@ -417,7 +462,12 @@
"showWeekNumbers": "Показывать номера недель",
"showWeekends": "Показывать выходные",
"firstDayOfWeek": "Первый день недели",
"layoutDateField": "Вид календаря"
}
"layoutDateField": "Вид календаря",
"noDateTitle": "No Date",
"noDateHint": "Unscheduled events will show up here",
"clickToAdd": "Click to add to the calendar",
"name": "Calendar layout"
},
"referencedCalendarPrefix": "Вид"
}
}
@@ -0,0 +1,261 @@
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
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('update calendar layout', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// open setting
await tester.tapDatabaseSettingButton();
await tester.tapDatabaseLayoutButton();
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.tapDatabaseSettingButton();
await tester.tapDatabaseLayoutButton();
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid);
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid);
await tester.pumpAndSettle();
});
testWidgets('calendar start from day setting', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create calendar view
await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar');
// Open setting
await tester.tapDatabaseSettingButton();
await tester.tapCalendarLayoutSettingButton();
// select the first day of week is Monday
await tester.tapFirstDayOfWeek();
await tester.tapFirstDayOfWeekStartFromMonday();
// Open the other page and open the new calendar page again
await tester.openPage(readme);
await tester.pumpAndSettle(const Duration(milliseconds: 300));
await tester.openPage('calendar');
// Open setting again and check the start from Monday is selected
await tester.tapDatabaseSettingButton();
await tester.tapCalendarLayoutSettingButton();
await tester.tapFirstDayOfWeek();
tester.assertFirstDayOfWeekStartFromMonday();
await tester.pumpAndSettle();
});
testWidgets('create new calendar event using new button', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Hover over today's calendar cell
await tester.hoverOnTodayCalendarCell();
// Tap on create new event button
await tester.tapAddCalendarEventButton();
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(1);
// Create another event on the same day
await tester.hoverOnTodayCalendarCell();
await tester.tapAddCalendarEventButton();
await tester.dismissRowDetailPage();
tester.assertNumberOfEventsInCalendar(2);
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
});
testWidgets('create new calendar event by double-clicking', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(1);
// Create another event on the same day
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
tester.assertNumberOfEventsInCalendar(2);
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
});
testWidgets('duplicate/delete a calendar event', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
// Duplicate the event
await tester.tapRowDetailPageDuplicateRowButton();
await tester.dismissRowDetailPage();
// Check that there are 2 events
tester.assertNumberOfEventsInCalendar(2);
// Delete an event
await tester.openCalendarEvent(index: 0);
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 1 event
tester.assertNumberOfEventsInCalendar(1);
});
testWidgets('edit the title of a calendar date event', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
// Click on the event
await tester.openCalendarEvent(index: 0);
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
});
testWidgets(
'edit the date of the date field being used to layout the calendar',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(DateTime.now());
await tester.dismissRowDetailPage();
// Make sure that the event is today
tester.assertNumberofEventsOnSpecificDay(1, DateTime.now());
// Click on the event
await tester.openCalendarEvent(index: 0);
// Open the date editor of the event
await tester.tapDateCellInRowDetailPage();
await tester.findDateEditor(findsOneWidget);
// Edit the event's date
final tomorrow = DateTime.now().add(const Duration(days: 1));
await tester.selectDay(content: tomorrow.day);
await tester.dismissCellEditor();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberofEventsOnSpecificDay(1, tomorrow);
});
testWidgets('reschedule an event by drag-and-drop', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event on the first of this month
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
// Make sure that the event has been rescheduled to the new date
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberofEventsOnSpecificDay(
1,
firstOfThisMonth.add(const Duration(days: 7)),
);
});
});
}
@@ -1,6 +1,8 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
@@ -46,6 +48,44 @@ void main() {
await tester.pumpAndSettle();
});
// Makesure the text cells are filled with the right content when there are
// multiple text cell
testWidgets('edit multiple text cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid');
await tester.createField(FieldType.RichText, 'description');
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'hello',
);
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'world',
cellIndex: 1,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'hello',
cellIndex: 0,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'world',
cellIndex: 1,
);
await tester.pumpAndSettle();
});
testWidgets('edit number cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -176,8 +216,235 @@ void main() {
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Select the date
await tester.selectDay(content: 3);
// Toggle include time
await tester.toggleIncludeTime();
// Dismiss the cell editor
await tester.dismissCellEditor();
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Turn off include time
await tester.toggleIncludeTime();
// Select a date
final today = DateTime.now();
await tester.selectDay(content: today.day);
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM d, y').format(today),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Toggle include time
final now = DateTime.now();
await tester.toggleIncludeTime();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM d, y HH:mm').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Change date format
await tester.changeDateFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y HH:mm').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Change time format
await tester.changeTimeFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y hh:mm a').format(now),
);
await tester.pumpAndSettle();
});
testWidgets('edit single select cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
const fieldType = FieldType.SingleSelect;
await tester.tapAddButton();
// When create a grid, it will create a single select field by default
await tester.tapCreateGridButton();
// Tap the cell to invoke the selection option editor
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create a new select option
await tester.createOption(name: 'tag 1');
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 1',
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create another select option
await tester.createOption(name: 'tag 2');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 2',
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// switch to first option
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 1',
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Deselect the currently-selected option
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
await tester.pumpAndSettle();
});
testWidgets('edit multi select cell', (tester) async {
final tags = [
'tag 1',
'tag 2',
'tag 3',
'tag 4',
];
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
const fieldType = FieldType.MultiSelect;
await tester.createField(fieldType, fieldType.name);
// Tap the cell to invoke the selection option editor
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create a new select option
await tester.createOption(name: tags.first);
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags.first,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create some other select options
await tester.createOption(name: tags[1]);
await tester.createOption(name: tags[2]);
await tester.createOption(name: tags[3]);
await tester.dismissCellEditor();
for (final tag in tags) {
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tag,
);
}
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(4),
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Deselect all options
for (final tag in tags) {
await tester.selectOption(name: tag);
}
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Select some options
await tester.selectOption(name: tags[1]);
await tester.selectOption(name: tags[3]);
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[1],
);
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[3],
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(2),
);
await tester.pumpAndSettle();
});
@@ -114,7 +114,6 @@ void main() {
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
await tester.createField(FieldType.RichText, 'New field 1');
// Delete the field
await tester.tapGridFieldWithName('New field 1');
@@ -0,0 +1,158 @@
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
const location = 'import_files';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('add text filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name');
await tester.tapFilterButtonInGrid('Name');
// enter 'A' in the filter text field
await tester.assertNumberOfRowsInGridPage(10);
await tester.enterTextInTextFilter('A');
await tester.assertNumberOfRowsInGridPage(1);
// after remove the filter, the grid should show all rows
await tester.enterTextInTextFilter('');
await tester.assertNumberOfRowsInGridPage(10);
await tester.enterTextInTextFilter('B');
await tester.assertNumberOfRowsInGridPage(1);
// open the menu to delete the filter
await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
await tester.assertNumberOfRowsInGridPage(10);
await tester.pumpAndSettle();
});
testWidgets('add checkbox filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');
await tester.assertNumberOfRowsInGridPage(5);
await tester.tapFilterButtonInGrid('Done');
await tester.tapCheckboxFilterButtonInGrid();
await tester.tapUnCheckedButtonOnCheckboxFilter();
await tester.assertNumberOfRowsInGridPage(5);
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
await tester.assertNumberOfRowsInGridPage(10);
await tester.pumpAndSettle();
});
testWidgets('add checklist filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');
// By default, the condition of checklist filter is 'uncompleted'
await tester.assertNumberOfRowsInGridPage(9);
await tester.tapFilterButtonInGrid('checklist');
await tester.tapChecklistFilterButtonInGrid();
await tester.tapCompletedButtonOnChecklistFilter();
await tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
testWidgets('add single select filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type');
await tester.tapFilterButtonInGrid('Type');
// select the option 's6'
await tester.tapOptionFilterWithName('s6');
await tester.assertNumberOfRowsInGridPage(0);
// unselect the option 's6'
await tester.tapOptionFilterWithName('s6');
await tester.assertNumberOfRowsInGridPage(10);
// select the option 's5'
await tester.tapOptionFilterWithName('s5');
await tester.assertNumberOfRowsInGridPage(1);
// select the option 's4'
await tester.tapOptionFilterWithName('s4');
// The row with 's4' or 's5' should be shown.
await tester.assertNumberOfRowsInGridPage(2);
await tester.pumpAndSettle();
});
testWidgets('add multi select filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.MultiSelect,
'multi-select',
);
await tester.tapFilterButtonInGrid('multi-select');
await tester.scrollOptionFilterListByOffset(const Offset(0, -200));
// select the option 'm1'. Any option with 'm1' should be shown.
await tester.tapOptionFilterWithName('m1');
await tester.assertNumberOfRowsInGridPage(5);
await tester.tapOptionFilterWithName('m1');
// select the option 'm2'. Any option with 'm2' should be shown.
await tester.tapOptionFilterWithName('m2');
await tester.assertNumberOfRowsInGridPage(4);
await tester.tapOptionFilterWithName('m2');
// select the option 'm4'. Any option with 'm4' should be shown.
await tester.tapOptionFilterWithName('m4');
await tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
});
}
@@ -28,7 +28,7 @@ void main() {
await TestFolder.cleanTestLocation(null);
});
testWidgets('open first row of the grid', (tester) async {
testWidgets('row details page opens', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -38,6 +38,9 @@ void main() {
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Make sure that the row page is opened
tester.assertRowDetailPageOpened();
});
testWidgets('insert emoji in the row detail page', (tester) async {
@@ -83,7 +86,7 @@ void main() {
// The emoji already displayed in the row banner
final emojiText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.title == '😅',
(widget) => widget is FlowyText && widget.text == '😅',
);
// The number of emoji should be two. One in the row displayed in the grid
@@ -109,7 +112,7 @@ void main() {
// Remove the emoji
await tester.tapButton(find.byType(RemoveEmojiButton));
final emojiText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.title == '😀',
(widget) => widget is FlowyText && widget.text == '😀',
);
expect(emojiText, findsNothing);
});
@@ -0,0 +1,215 @@
import 'dart:io';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
import 'package:path/path.dart' as p;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
const location = 'import_files';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('import v0.2.0 database data', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
await tester.tapAddButton();
await tester.tapImportButton();
final testFileNames = ['v020.afdb'];
final fileLocation = await tester.currentFileLocation();
for (final fileName in testFileNames) {
final str = await rootBundle.loadString(
p.join(
'assets/test/workspaces/database',
fileName,
),
);
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
}
// mock get files
await mockPickFilePaths(testFileNames, name: location);
await tester.tapDatabaseRawDataButton();
await tester.openPage('v020');
// check the import content
// await tester.assertCellContent(
// rowIndex: 7,
// fieldType: FieldType.RichText,
// // fieldName: 'Name',
// content: '',
// );
// check the text cell
final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];
for (final (index, content) in textCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
// check the checkbox cell
final checkboxCells = <bool>[
true,
true,
true,
true,
true,
false,
false,
false,
false,
false
];
for (final (index, content) in checkboxCells.indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// check the number cell
final numberCells = <String>[
'-1',
'-2',
'0.1',
'0.2',
'1',
'2',
'10',
'11',
'12',
''
];
for (final (index, content) in numberCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// check the url cell
final urlCells = <String>[
'appflowy.io',
'no url',
'appflowy.io',
'https://github.com/AppFlowy-IO/',
'',
'',
];
for (final (index, content) in urlCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.URL,
content: content,
);
}
// check the single select cell
final singleSelectCells = <String>[
's1',
's2',
's3',
's4',
's5',
'',
'',
'',
'',
'',
];
for (final (index, content) in singleSelectCells.indexed) {
await tester.assertSingleSelectOption(
rowIndex: index,
content: content,
);
}
// check the multi select cell
final List<List<String>> multiSelectCells = [
['m1'],
['m1', 'm2'],
['m1', 'm2', 'm3'],
['m1', 'm2', 'm3'],
['m1', 'm2', 'm3', 'm4', 'm5'],
[],
[],
[],
[],
[],
];
for (final (index, contents) in multiSelectCells.indexed) {
await tester.assertMultiSelectOption(
rowIndex: index,
contents: contents,
);
}
// check the checklist cell
final List<double> checklistCells = [
0.6,
0.3,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
];
for (final (index, percent) in checklistCells.indexed) {
await tester.assertChecklistCellInGrid(
rowIndex: index,
percent: percent,
);
}
// check the date cell
final List<String> dateCells = [
'Jun 01, 2023',
'Jun 02, 2023',
'Jun 03, 2023',
'Jun 04, 2023',
'Jun 05, 2023',
'Jun 05, 2023',
'Jun 16, 2023',
'',
'',
''
];
for (final (index, content) in dateCells.indexed) {
await tester.assertDateCellInGrid(
rowIndex: index,
fieldType: FieldType.DateTime,
content: content,
);
}
});
});
}
@@ -0,0 +1,283 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
const location = 'import_files';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('add text sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
// check the text cell order
final textCells = <String>[
'',
'',
'',
'',
'',
'A',
'B',
'C',
'D',
'E',
];
for (final (index, content) in textCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Name');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'E',
'D',
'C',
'B',
'A',
'',
'',
'',
'',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
// delete all sorts
await tester.tapSortMenuInSettingBar();
await tester.tapAllSortButton();
// check the text cell order
for (final (index, content) in <String>[
'A',
'B',
'C',
'D',
'E',
'',
'',
'',
'',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
await tester.pumpAndSettle();
});
testWidgets('add checkbox sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
// check the checkbox cell order
for (final (index, content) in <bool>[
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Done');
await tester.tapSortByDescending();
for (final (index, content) in <bool>[
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
await tester.pumpAndSettle();
});
testWidgets('add number sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
// check the number cell order
for (final (index, content) in <String>[
'',
'-2',
'-1',
'0.1',
'0.2',
'1',
'2',
'10',
'11',
'12',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('number');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'1',
'0.2',
'0.1',
'-1',
'-2',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
await tester.pumpAndSettle();
});
testWidgets('add number and text sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
// open the sort menu and select number order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('number');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'1',
'0.2',
'0.1',
'-1',
'-2',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
await tester.tapSortMenuInSettingBar();
await tester.tapCreateSortByFieldTypeInSortMenu(
FieldType.RichText,
'Name',
);
// check number cell order
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'',
'-1',
'-2',
'0.1',
'0.2',
'1',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// check text cell order
for (final (index, content) in <String>[
'',
'',
'',
'',
'',
'A',
'B',
'C',
'D',
'E',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
await tester.pumpAndSettle();
});
});
}
@@ -0,0 +1,96 @@
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
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('create linked view', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// Create grid view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
// Create calendar view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
await tester.pumpAndSettle();
});
testWidgets('rename and delete linked view', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// rename board view
await tester.renameLinkedView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
'new board',
);
final findBoard = tester.findTabBarLinkViewByViewName('new board');
expect(findBoard, findsOneWidget);
// delete the board
await tester.deleteDatebaseView(findBoard);
expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing);
await tester.pumpAndSettle();
});
testWidgets('delete the last database view', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// delete the board
await tester.deleteDatebaseView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
);
await tester.pumpAndSettle();
});
});
}
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -7,7 +7,7 @@ import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -0,0 +1,141 @@
import 'dart:io';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/ime.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('edit document', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('redo & undo', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document called Sample
const pageName = 'Sample';
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
// insert 1. to trigger it to be a numbered list
await tester.ime.insertText('1. ');
expect(find.text('1.', findRichText: true), findsOneWidget);
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
NumberedListBlockKeys.type,
);
// undo
// numbered list will be reverted to paragraph
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
ParagraphBlockKeys.type,
);
// redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
isShiftPressed: true,
);
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
NumberedListBlockKeys.type,
);
// switch to other page and switch back
await tester.openPage(readme);
await tester.openPage(pageName);
// the numbered list should be kept
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
NumberedListBlockKeys.type,
);
});
testWidgets('write a readme document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document called Sample
const pageName = 'Sample';
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
// mock inputting the sample
final lines = _sample.split('\n');
for (final line in lines) {
await tester.ime.insertText(line);
await tester.ime.insertCharacter('\n');
}
// switch to other page and switch back
await tester.openPage(readme);
await tester.openPage(pageName);
// this screenshots are different on different platform, so comment it out temporarily.
// check the document
// await expectLater(
// find.byType(AppFlowyEditor),
// matchesGoldenFile('document/edit_document_test.png'),
// );
});
});
}
// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet.
const _sample = r'''
# Heading 1
## Heading 2
### Heading 3
---
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
[] Type / followed by /bullet or /num to create a list.
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
---
* bulleted list 1
* bulleted list 2
* bulleted list 3
bulleted list 4
---
1. numbered list 1
2. numbered list 2
3. numbered list 3
numbered list 4
---
" quote''';
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

@@ -1,11 +1,23 @@
import 'package:integration_test/integration_test.dart';
import 'switch_folder_test.dart' as switch_folder_test;
import 'document_test.dart' as document_test;
import 'cover_image_test.dart' as cover_image_test;
import 'document/document_test.dart' as document_test;
import 'document/cover_image_test.dart' as cover_image_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'import_files_test.dart' as import_files_test;
import 'document_with_database_test.dart' as document_with_database_test;
import 'document/document_with_database_test.dart'
as document_with_database_test;
import 'document/edit_document_test.dart' as edit_document_test;
import 'database_cell_test.dart' as database_cell_test;
import 'database_field_test.dart' as database_field_test;
import 'database_share_test.dart' as database_share_test;
import 'database_row_page_test.dart' as database_row_page_test;
import 'database_row_test.dart' as database_row_test;
import 'database_setting_test.dart' as database_setting_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_view_test.dart' as database_view_test;
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_sort_test.dart' as database_sort_test;
/// The main task runner for all integration tests in AppFlowy.
///
@@ -17,11 +29,27 @@ import 'document_with_database_test.dart' as document_with_database_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
switch_folder_test.main();
cover_image_test.main();
document_test.main();
share_markdown_test.main();
import_files_test.main();
// Document integration tests
cover_image_test.main();
document_test.main();
document_with_database_test.main();
edit_document_test.main();
// Database integration tests
database_cell_test.main();
database_field_test.main();
database_share_test.main();
database_row_page_test.main();
database_row_test.main();
database_setting_test.main();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
// board_test.main();
// empty_document_test.main();
// smart_menu_test.main();
@@ -1,9 +1,12 @@
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:flowy_infra/uuid.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';
import 'package:path/path.dart' as p;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -25,12 +28,12 @@ void main() {
});
testWidgets('switch to B from A, then switch to A again', (tester) async {
const String userA = 'userA';
const String userB = 'userB';
final userA = uuid();
final userB = uuid();
await TestFolder.cleanTestLocation(userA);
await TestFolder.cleanTestLocation(userB);
await TestFolder.setTestLocation(userA);
await TestFolder.setTestLocation(p.join(userA, appFlowyDataFolder));
await tester.initializeAppFlowy();
@@ -1,10 +1,12 @@
import 'dart:io';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/main.dart' as app;
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider/path_provider.dart';
@@ -42,7 +44,7 @@ class TestFolder {
/// Get default location under development environment.
static Future<String> defaultDevelopmentLocation() async {
final dir = await appFlowyDocumentDirectory();
final dir = await appFlowyApplicationDataDirectory();
return dir.path;
}
@@ -60,18 +62,18 @@ class TestFolder {
extension AppFlowyTestBase on WidgetTester {
Future<void> initializeAppFlowy() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('hotkey_manager'),
(MethodCall methodCall) async {
if (methodCall.method == 'unregisterAll') {
// do nothing
}
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
(MethodCall methodCall) async {
if (methodCall.method == 'unregisterAll') {
// do nothing
}
return;
},
);
return;
});
WidgetsFlutterBinding.ensureInitialized();
await FlowyRunner.run(FlowyApp(), IntegrationMode.integrationTest);
await app.main();
await wait(3000);
await pumpAndSettle(const Duration(seconds: 2));
}
@@ -83,7 +85,11 @@ extension AppFlowyTestBase on WidgetTester {
bool warnIfMissed = true,
int milliseconds = 500,
}) async {
await tap(finder);
await tap(
finder,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
}
@@ -99,7 +105,7 @@ extension AppFlowyTestBase on WidgetTester {
);
if (button.evaluate().isEmpty) {
button = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.title == tr,
(widget) => widget is FlowyText && widget.text == tr,
);
}
await tapButton(
@@ -121,6 +127,29 @@ extension AppFlowyTestBase on WidgetTester {
return;
}
Future<void> doubleTapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = true,
int milliseconds = 500,
}) async {
await tapButton(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
milliseconds: kDoubleTapMinTime.inMilliseconds,
);
await tapButton(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
milliseconds: milliseconds,
);
}
Future<void> wait(int milliseconds) async {
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
@@ -130,7 +159,7 @@ extension AppFlowyTestBase on WidgetTester {
extension AppFlowyFinderTestBase on CommonFinders {
Finder findTextInFlowyText(String text) {
return find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.title == text,
(widget) => widget is FlowyText && widget.text == text,
);
}
}
@@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
@@ -47,6 +48,13 @@ extension CommonOperations on WidgetTester {
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddButton] first.
Future<void> tapCreateCalendarButton() async {
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
}
/// Tap the import button.
///
/// Must call [tapAddButton] first.
@@ -140,6 +148,13 @@ extension CommonOperations on WidgetTester {
}
}
/// open the page with given name.
Future<void> openPage(String name) async {
final finder = findPageName(name);
expect(finder, findsOneWidget);
await tapButton(finder);
}
/// Tap the ... button beside the page name.
///
/// Must call [hoverOnPageName] first.
@@ -230,6 +245,42 @@ extension CommonOperations on WidgetTester {
);
await pumpAndSettle();
}
Future<void> simulateKeyEvent(
LogicalKeyboardKey key, {
bool isControlPressed = false,
bool isShiftPressed = false,
bool isAltPressed = false,
bool isMetaPressed = false,
}) async {
if (isControlPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.control);
}
if (isShiftPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.shift);
}
if (isAltPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.alt);
}
if (isMetaPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.meta);
}
await simulateKeyDownEvent(key);
await simulateKeyUpEvent(key);
if (isControlPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.control);
}
if (isShiftPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
}
if (isAltPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.alt);
}
if (isMetaPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.meta);
}
await pumpAndSettle();
}
}
extension ViewLayoutPBTest on ViewLayoutPB {
File diff suppressed because it is too large. Load diff
@@ -13,6 +13,12 @@ class EditorOperations {
final WidgetTester tester;
EditorState getCurrentEditorState() {
return tester
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
.editorState;
}
/// Tap the line of editor at [index]
Future<void> tapLineOfEditorAt(int index) async {
final textBlocks = find.byType(TextBlockComponentWidget);
@@ -12,7 +12,7 @@ extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
void expectToSeeHomePage() {
expect(find.byType(HomeStack), findsOneWidget);
expect(find.textContaining(readme), findsOneWidget);
expect(find.textContaining(readme), findsWidgets);
}
/// Expect to see the page name on the home page.
@@ -42,7 +42,7 @@ extension Expectation on WidgetTester {
final exportSuccess = find.byWidgetPredicate(
(widget) =>
widget is FlowyText &&
widget.title == LocaleKeys.settings_files_exportFileSuccess.tr(),
widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(),
);
expect(exportSuccess, findsOneWidget);
}
@@ -62,7 +62,7 @@ extension Expectation on WidgetTester {
/// Expect to see the user name on the home page
void expectToSeeUserName(String name) {
final userName = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.title == name,
(widget) => widget is FlowyText && widget.text == name,
);
expect(userName, findsOneWidget);
}
@@ -72,7 +72,7 @@ extension Expectation on WidgetTester {
Finder textWidget = find.textContaining(text, findRichText: true);
if (textWidget.evaluate().isEmpty) {
textWidget = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.title == text,
(widget) => widget is FlowyText && widget.text == text,
);
}
expect(textWidget, findsOneWidget);
@@ -82,6 +82,7 @@ extension Expectation on WidgetTester {
Finder findPageName(String name) {
return find.byWidgetPredicate(
(widget) => widget is ViewSectionItem && widget.view.name == name,
skipOffstage: false,
);
}
}
+4 -1
View File
@@ -6,5 +6,8 @@ import 'startup/startup.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlowyRunner.run(FlowyApp());
await FlowyRunner.run(
FlowyApp(),
integrationEnv(),
);
}
@@ -1,8 +1,8 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart';
@@ -15,6 +15,7 @@ import 'dart:async';
import 'package:dartz/dartz.dart';
import 'database_view_service.dart';
import 'defines.dart';
import 'layout/layout_service.dart';
import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart';
import 'group/group_listener.dart';
@@ -49,16 +50,11 @@ class DatabaseLayoutSettingCallbacks {
});
}
class CalendarLayoutCallbacks {
final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged;
CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
}
class DatabaseCallbacks {
OnDatabaseChanged? onDatabaseChanged;
OnFieldsChanged? onFieldsChanged;
OnFiltersChanged? onFiltersChanged;
OnSortsChanged? onSortsChanged;
OnNumOfRowsChanged? onNumOfRowsChanged;
OnRowsDeleted? onRowsDeleted;
OnRowsUpdated? onRowsUpdated;
@@ -69,6 +65,7 @@ class DatabaseCallbacks {
this.onNumOfRowsChanged,
this.onFieldsChanged,
this.onFiltersChanged,
this.onSortsChanged,
this.onRowsUpdated,
this.onRowsDeleted,
this.onRowsCreated,
@@ -79,15 +76,14 @@ class DatabaseController {
final String viewId;
final DatabaseViewBackendService _databaseViewBackendSvc;
final FieldController fieldController;
DatabaseLayoutPB? databaseLayout;
DatabaseLayoutPB databaseLayout;
DatabaseLayoutSettingPB? databaseLayoutSetting;
late DatabaseViewCache _viewCache;
// Callbacks
DatabaseCallbacks? _databaseCallbacks;
GroupCallbacks? _groupCallbacks;
DatabaseLayoutSettingCallbacks? _layoutCallbacks;
CalendarLayoutCallbacks? _calendarLayoutCallbacks;
final List<DatabaseCallbacks> _databaseCallbacks = [];
final List<GroupCallbacks> _groupCallbacks = [];
final List<DatabaseLayoutSettingCallbacks> _layoutCallbacks = [];
// Getters
RowCache get rowCache => _viewCache.rowCache;
@@ -95,15 +91,14 @@ class DatabaseController {
// Listener
final DatabaseGroupListener _groupListener;
final DatabaseLayoutSettingListener _layoutListener;
final DatabaseCalendarLayoutListener _calendarLayoutListener;
DatabaseController({required ViewPB view})
: viewId = view.id,
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
fieldController = FieldController(viewId: view.id),
_groupListener = DatabaseGroupListener(view.id),
_layoutListener = DatabaseLayoutSettingListener(view.id),
_calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
databaseLayout = databaseLayoutFromViewLayout(view.layout),
_layoutListener = DatabaseLayoutSettingListener(view.id) {
_viewCache = DatabaseViewCache(
viewId: viewId,
fieldController: fieldController,
@@ -114,44 +109,51 @@ class DatabaseController {
_listenOnLayoutChanged();
}
void setListener({
void addListener({
DatabaseCallbacks? onDatabaseChanged,
DatabaseLayoutSettingCallbacks? onLayoutChanged,
GroupCallbacks? onGroupChanged,
CalendarLayoutCallbacks? onCalendarLayoutChanged,
}) {
_layoutCallbacks = onLayoutChanged;
_databaseCallbacks = onDatabaseChanged;
_groupCallbacks = onGroupChanged;
_calendarLayoutCallbacks = onCalendarLayoutChanged;
if (onLayoutChanged != null) {
_layoutCallbacks.add(onLayoutChanged);
}
if (onDatabaseChanged != null) {
_databaseCallbacks.add(onDatabaseChanged);
}
if (onGroupChanged != null) {
_groupCallbacks.add(onGroupChanged);
}
}
Future<Either<Unit, FlowyError>> open() async {
return _databaseViewBackendSvc.openGrid().then((result) {
return _databaseViewBackendSvc.openDatabase().then((result) {
return result.fold(
(database) async {
(DatabasePB database) async {
databaseLayout = database.layoutType;
if (databaseLayout == DatabaseLayoutPB.Calendar) {
_listenOnCalendarLayoutChanged();
}
_databaseCallbacks?.onDatabaseChanged?.call(database);
_viewCache.rowCache.setInitialRows(database.rows);
return await fieldController
.loadFields(
// Load the actual database field data.
final fieldsOrFail = await fieldController.loadFields(
fieldIds: database.fields,
)
.then(
(result) {
return result.fold(
(l) => Future(() async {
await _loadGroups();
await _loadLayoutSetting();
return left(l);
}),
(err) => right(err),
);
);
return fieldsOrFail.fold(
(fields) {
// Notify the database is changed after the fields are loaded.
// The database won't can't be used until the fields are loaded.
for (final callback in _databaseCallbacks) {
callback.onDatabaseChanged?.call(database);
}
_viewCache.rowCache.setInitialRows(database.rows);
return Future(() async {
await _loadGroups();
await _loadLayoutSetting();
return left(fields);
});
},
(err) {
Log.error(err);
return right(err);
},
);
},
@@ -212,11 +214,14 @@ class DatabaseController {
);
}
Future<void> updateCalenderLayoutSetting(
CalendarLayoutSettingPB layoutSetting,
Future<void> updateLayoutSetting(
CalendarLayoutSettingPB calendarlLayoutSetting,
) async {
await _databaseViewBackendSvc
.updateLayoutSetting(calendarLayoutSetting: layoutSetting)
.updateLayoutSetting(
calendarLayoutSetting: calendarlLayoutSetting,
layoutType: databaseLayout,
)
.then((result) {
result.fold((l) => null, (r) => Log.error(r));
});
@@ -227,10 +232,9 @@ class DatabaseController {
await fieldController.dispose();
await _groupListener.stop();
await _viewCache.dispose();
_databaseCallbacks = null;
_groupCallbacks = null;
_layoutCallbacks = null;
_calendarLayoutCallbacks = null;
_databaseCallbacks.clear();
_groupCallbacks.clear();
_layoutCallbacks.clear();
}
Future<void> _loadGroups() async {
@@ -238,7 +242,9 @@ class DatabaseController {
return Future(
() => result.fold(
(groups) {
_groupCallbacks?.onGroupByField?.call(groups.items);
for (final callback in _groupCallbacks) {
callback.onGroupByField?.call(groups.items);
}
},
(err) => Log.error(err),
),
@@ -246,46 +252,63 @@ class DatabaseController {
}
Future<void> _loadLayoutSetting() async {
if (databaseLayout != null) {
_databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) {
result.fold(
(newDatabaseLayoutSetting) {
databaseLayoutSetting = newDatabaseLayoutSetting;
databaseLayoutSetting?.freeze();
_databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) {
result.fold(
(newDatabaseLayoutSetting) {
databaseLayoutSetting = newDatabaseLayoutSetting;
databaseLayoutSetting?.freeze();
_layoutCallbacks?.onLoadLayout(newDatabaseLayoutSetting);
},
(r) => Log.error(r),
);
});
}
for (final callback in _layoutCallbacks) {
callback.onLoadLayout(newDatabaseLayoutSetting);
}
},
(r) => Log.error(r),
);
});
}
void _listenOnRowsChanged() {
final callbacks = DatabaseViewCallbacks(
onNumOfRowsChanged: (rows, rowByRowId, reason) {
_databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
for (final callback in _databaseCallbacks) {
callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
}
},
onRowsDeleted: (ids) {
_databaseCallbacks?.onRowsDeleted?.call(ids);
for (final callback in _databaseCallbacks) {
callback.onRowsDeleted?.call(ids);
}
},
onRowsUpdated: (ids, reason) {
_databaseCallbacks?.onRowsUpdated?.call(ids, reason);
for (final callback in _databaseCallbacks) {
callback.onRowsUpdated?.call(ids, reason);
}
},
onRowsCreated: (ids) {
_databaseCallbacks?.onRowsCreated?.call(ids);
for (final callback in _databaseCallbacks) {
callback.onRowsCreated?.call(ids);
}
},
);
_viewCache.setListener(callbacks);
_viewCache.addListener(callbacks);
}
void _listenOnFieldsChanged() {
fieldController.addListener(
onReceiveFields: (fields) {
_databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields));
for (final callback in _databaseCallbacks) {
callback.onFieldsChanged?.call(UnmodifiableListView(fields));
}
},
onSorts: (sorts) {
for (final callback in _databaseCallbacks) {
callback.onSortsChanged?.call(sorts);
}
},
onFilters: (filters) {
_databaseCallbacks?.onFiltersChanged?.call(filters);
for (final callback in _databaseCallbacks) {
callback.onFiltersChanged?.call(filters);
}
},
);
}
@@ -296,15 +319,21 @@ class DatabaseController {
result.fold(
(changeset) {
if (changeset.updateGroups.isNotEmpty) {
_groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups);
for (final callback in _groupCallbacks) {
callback.onUpdateGroup?.call(changeset.updateGroups);
}
}
if (changeset.deletedGroups.isNotEmpty) {
_groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups);
for (final callback in _groupCallbacks) {
callback.onDeleteGroup?.call(changeset.deletedGroups);
}
}
for (final insertedGroup in changeset.insertedGroups) {
_groupCallbacks?.onInsertGroup?.call(insertedGroup);
for (final callback in _groupCallbacks) {
callback.onInsertGroup?.call(insertedGroup);
}
}
},
(r) => Log.error(r),
@@ -313,7 +342,9 @@ class DatabaseController {
onGroupByNewField: (result) {
result.fold(
(groups) {
_groupCallbacks?.onGroupByField?.call(groups);
for (final callback in _groupCallbacks) {
callback.onGroupByField?.call(groups);
}
},
(r) => Log.error(r),
);
@@ -325,24 +356,13 @@ class DatabaseController {
_layoutListener.start(
onLayoutChanged: (result) {
result.fold(
(newDatabaseLayoutSetting) {
databaseLayoutSetting = newDatabaseLayoutSetting;
(newLayout) {
databaseLayoutSetting = newLayout;
databaseLayoutSetting?.freeze();
_layoutCallbacks?.onLayoutChanged(newDatabaseLayoutSetting);
},
(r) => Log.error(r),
);
},
);
}
void _listenOnCalendarLayoutChanged() {
_calendarLayoutListener.start(
onCalendarLayoutChanged: (result) {
result.fold(
(l) {
_calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
for (final callback in _layoutCallbacks) {
callback.onLayoutChanged(newLayout);
}
},
(r) => Log.error(r),
);
@@ -25,7 +25,7 @@ class DatabaseViewBackendService {
.then((value) => value.leftMap((l) => l.value));
}
Future<Either<DatabasePB, FlowyError>> openGrid() async {
Future<Either<DatabasePB, FlowyError>> openDatabase() async {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();
}
@@ -113,9 +113,12 @@ class DatabaseViewBackendService {
}
Future<Either<Unit, FlowyError>> updateLayoutSetting({
required DatabaseLayoutPB layoutType,
CalendarLayoutSettingPB? calendarLayoutSetting,
}) {
final payload = LayoutSettingChangesetPB.create()..viewId = viewId;
final payload = LayoutSettingChangesetPB.create()
..viewId = viewId
..layoutType = layoutType;
if (calendarLayoutSetting != null) {
payload.calendar = calendarLayoutSetting;
}
@@ -1,5 +1,6 @@
import 'dart:collection';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -10,6 +11,7 @@ import 'row/row_service.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnSortsChanged = void Function(List<SortInfo>);
typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<RowId> ids);
@@ -33,7 +33,8 @@ class _GridFieldNotifier extends ChangeNotifier {
notifyListeners();
}
List<FieldInfo> get fieldInfos => _fieldInfos;
UnmodifiableListView<FieldInfo> get fieldInfos =>
UnmodifiableListView(_fieldInfos);
}
class _GridFilterNotifier extends ChangeNotifier {
@@ -85,9 +86,11 @@ class FieldController {
final FilterBackendService _filterBackendSvc;
final SortBackendService _sortBackendSvc;
bool _isDisposed = false;
// Field callbacks
final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
final _GridFieldNotifier _fieldNotifier = _GridFieldNotifier();
// Field updated callbacks
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
@@ -107,15 +110,15 @@ class FieldController {
final Map<String, SortPB> _sortPBByFieldId = {};
// Getters
List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];
List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
FieldInfo? getField(String fieldId) {
final fields = _fieldNotifier?.fieldInfos
.where((element) => element.id == fieldId)
.toList() ??
[];
final fields = _fieldNotifier.fieldInfos
.where((element) => element.id == fieldId)
.toList();
if (fields.isEmpty) {
return null;
}
@@ -169,6 +172,10 @@ class FieldController {
_listenOnSortChanged();
_settingBackendSvc.getSetting().then((result) {
if (_isDisposed) {
return;
}
result.fold(
(setting) => _updateSetting(setting),
(err) => Log.error(err),
@@ -257,6 +264,10 @@ class FieldController {
_filtersListener.start(
onFilterChanged: (result) {
if (_isDisposed) {
return;
}
result.fold(
(FilterChangesetNotificationPB changeset) {
final List<FilterInfo> filters = filterInfos;
@@ -351,6 +362,9 @@ class FieldController {
_sortsListener.start(
onSortChanged: (result) {
if (_isDisposed) {
return;
}
result.fold(
(SortChangesetNotificationPB changeset) {
final List<SortInfo> newSortInfos = sortInfos;
@@ -371,6 +385,10 @@ class FieldController {
//Listen on setting changes
_settingListener.start(
onSettingUpdated: (result) {
if (_isDisposed) {
return;
}
result.fold(
(setting) => _updateSetting(setting),
(r) => Log.error(r),
@@ -385,6 +403,9 @@ class FieldController {
onFieldsChanged: (result) {
result.fold(
(changeset) {
if (_isDisposed) {
return;
}
_deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields);
@@ -417,27 +438,29 @@ class FieldController {
}
void _updateFieldInfos() {
if (_fieldNotifier != null) {
for (final field in _fieldNotifier!.fieldInfos) {
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
field._hasFilter = _filterPBByFieldId[field.id] != null;
field._hasSort = _sortPBByFieldId[field.id] != null;
}
_fieldNotifier?.notify();
for (final field in _fieldNotifier.fieldInfos) {
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
field._hasFilter = _filterPBByFieldId[field.id] != null;
field._hasSort = _sortPBByFieldId[field.id] != null;
}
_fieldNotifier.notify();
}
Future<void> dispose() async {
if (_isDisposed) {
Log.warn('FieldController is already disposed');
return;
}
_isDisposed = true;
await _fieldListener.stop();
await _filtersListener.stop();
await _settingListener.stop();
await _sortsListener.stop();
for (final callback in _fieldCallbacks.values) {
_fieldNotifier?.removeListener(callback);
_fieldNotifier.removeListener(callback);
}
_fieldNotifier?.dispose();
_fieldNotifier = null;
_fieldNotifier.dispose();
for (final callback in _filterCallbacks.values) {
_filterNotifier?.removeListener(callback);
@@ -460,7 +483,11 @@ class FieldController {
return Future(
() => result.fold(
(newFields) {
_fieldNotifier?.fieldInfos =
if (_isDisposed) {
return left(unit);
}
_fieldNotifier.fieldInfos =
newFields.map((field) => FieldInfo(field: field)).toList();
_loadFilters();
_loadSorts();
@@ -551,7 +578,7 @@ class FieldController {
}
_fieldCallbacks[onReceiveFields] = callback;
_fieldNotifier?.addListener(callback);
_fieldNotifier.addListener(callback);
}
if (onFilters != null) {
@@ -588,7 +615,7 @@ class FieldController {
if (onFieldsListener != null) {
final callback = _fieldCallbacks.remove(onFieldsListener);
if (callback != null) {
_fieldNotifier?.removeListener(callback);
_fieldNotifier.removeListener(callback);
}
}
if (onFiltersListener != null) {
@@ -616,7 +643,7 @@ class FieldController {
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fieldInfos = newFields;
_fieldNotifier.fieldInfos = newFields;
}
void _insertFields(List<IndexFieldPB> insertedFields) {
@@ -632,7 +659,7 @@ class FieldController {
newFieldInfos.add(fieldInfo);
}
}
_fieldNotifier?.fieldInfos = newFieldInfos;
_fieldNotifier.fieldInfos = newFieldInfos;
}
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
@@ -654,7 +681,7 @@ class FieldController {
}
if (updatedFields.isNotEmpty) {
_fieldNotifier?.fieldInfos = newFields;
_fieldNotifier.fieldInfos = newFields;
}
return updatedFields;
}
@@ -15,13 +15,13 @@ class DatabaseLayoutBackendService {
}) {
final payload = UpdateViewPayloadPB.create()
..viewId = viewId
..layout = _viewLayoutFromDatabaseLayout(layout);
..layout = viewLayoutFromDatabaseLayout(layout);
return FolderEventUpdateView(payload).send();
}
}
ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
switch (databaseLayout) {
case DatabaseLayoutPB.Board:
return ViewLayoutPB.Board;
@@ -33,3 +33,16 @@ ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
throw UnimplementedError;
}
}
DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) {
switch (viewLayout) {
case ViewLayoutPB.Board:
return DatabaseLayoutPB.Board;
case ViewLayoutPB.Calendar:
return DatabaseLayoutPB.Calendar;
case ViewLayoutPB.Grid:
return DatabaseLayoutPB.Grid;
default:
throw UnimplementedError;
}
}
@@ -1,103 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart';
part 'setting_bloc.freezed.dart';
class DatabaseSettingBloc
extends Bloc<DatabaseSettingEvent, DatabaseSettingState> {
final String viewId;
DatabaseSettingBloc({required this.viewId})
: super(DatabaseSettingState.initial()) {
on<DatabaseSettingEvent>(
(event, emit) async {
event.map(
performAction: (_PerformAction value) {
emit(state.copyWith(selectedAction: Some(value.action)));
},
);
},
);
}
}
@freezed
class DatabaseSettingEvent with _$DatabaseSettingEvent {
const factory DatabaseSettingEvent.performAction(
DatabaseSettingAction action,
) = _PerformAction;
}
@freezed
class DatabaseSettingState with _$DatabaseSettingState {
const factory DatabaseSettingState({
required Option<DatabaseSettingAction> selectedAction,
}) = _DatabaseSettingState;
factory DatabaseSettingState.initial() => DatabaseSettingState(
selectedAction: none(),
);
}
enum DatabaseSettingAction {
showProperties,
showLayout,
showGroup,
showCalendarLayout,
}
extension DatabaseSettingActionExtension on DatabaseSettingAction {
String iconName() {
switch (this) {
case DatabaseSettingAction.showProperties:
return 'grid/setting/properties';
case DatabaseSettingAction.showLayout:
return 'grid/setting/database_layout';
case DatabaseSettingAction.showGroup:
return 'grid/setting/group';
case DatabaseSettingAction.showCalendarLayout:
return 'grid/setting/calendar_layout';
}
}
String title() {
switch (this) {
case DatabaseSettingAction.showProperties:
return LocaleKeys.grid_settings_Properties.tr();
case DatabaseSettingAction.showLayout:
return LocaleKeys.grid_settings_databaseLayout.tr();
case DatabaseSettingAction.showGroup:
return LocaleKeys.grid_settings_group.tr();
case DatabaseSettingAction.showCalendarLayout:
return LocaleKeys.calendar_settings_name.tr();
}
}
}
/// Returns the list of actions that should be shown for the given database layout.
List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showGroup,
];
case DatabaseLayoutPB.Calendar:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showCalendarLayout,
];
case DatabaseLayoutPB.Grid:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
];
default:
return [];
}
}
@@ -0,0 +1,290 @@
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'database_controller.dart';
import 'database_view_service.dart';
part 'tar_bar_bloc.freezed.dart';
class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> {
GridTabBarBloc({
bool isInlineView = false,
required ViewPB view,
}) : super(GridTabBarState.initial(view)) {
on<GridTabBarEvent>(
(event, emit) async {
event.when(
initial: () {
_listenInlineViewChanged();
_loadChildView();
},
didLoadChildViews: (List<ViewPB> childViews) {
emit(
state.copyWith(
tabBars: [
...state.tabBars,
...childViews.map(
(newChildView) => TarBar(view: newChildView),
),
],
tabBarControllerByViewId: _extendsTabBarController(childViews),
),
);
},
selectView: (String viewId) {
final index =
state.tabBars.indexWhere((element) => element.viewId == viewId);
if (index != -1) {
emit(
state.copyWith(selectedIndex: index),
);
}
},
createView: (action) {
_createLinkedView(action.name, action.layoutType);
},
deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId);
result.fold(
(l) {},
(r) => Log.error(r),
);
},
renameView: (String viewId, String newName) {
ViewBackendService.updateView(viewId: viewId, name: newName);
},
didUpdateChildViews: (updatePB) async {
if (updatePB.createChildViews.isNotEmpty) {
final allTabBars = [
...state.tabBars,
...updatePB.createChildViews.map((e) => TarBar(view: e))
];
emit(
state.copyWith(
tabBars: allTabBars,
selectedIndex: state.tabBars.length,
tabBarControllerByViewId:
_extendsTabBarController(updatePB.createChildViews),
),
);
}
if (updatePB.deleteChildViews.isNotEmpty) {
final allTabBars = [...state.tabBars];
final tabBarControllerByViewId = {
...state.tabBarControllerByViewId
};
var newSelectedIndex = state.selectedIndex;
for (final viewId in updatePB.deleteChildViews) {
final index = allTabBars.indexWhere(
(element) => element.viewId == viewId,
);
if (index != -1) {
final tarBar = allTabBars.removeAt(index);
// Dispose the controller when the tab is removed.
final controller =
tabBarControllerByViewId.remove(tarBar.viewId);
controller?.dispose();
}
if (index == state.selectedIndex) {
if (index > 0 && allTabBars.isNotEmpty) {
newSelectedIndex = index - 1;
}
}
}
emit(
state.copyWith(
tabBars: allTabBars,
selectedIndex: newSelectedIndex,
tabBarControllerByViewId: tabBarControllerByViewId,
),
);
}
},
viewDidUpdate: (ViewPB updatedView) {
final index = state.tabBars.indexWhere(
(element) => element.viewId == updatedView.id,
);
if (index != -1) {
final allTabBars = [...state.tabBars];
final updatedTabBar = TarBar(view: updatedView);
allTabBars[index] = updatedTabBar;
emit(state.copyWith(tabBars: allTabBars));
}
},
);
},
);
}
@override
Future<void> close() async {
for (final tabBar in state.tabBars) {
await state.tabBarControllerByViewId[tabBar.viewId]?.dispose();
}
return super.close();
}
void _listenInlineViewChanged() {
final controller = state.tabBarControllerByViewId[state.parentView.id];
controller?.onViewUpdated = (newView) {
add(GridTabBarEvent.viewDidUpdate(newView));
};
// Only listen the child view changes when the parent view is inline.
controller?.onViewChildViewChanged = (update) {
add(GridTabBarEvent.didUpdateChildViews(update));
};
}
/// Create tab bar controllers for the new views and return the updated map.
Map<String, DatabaseTarBarController> _extendsTabBarController(
List<ViewPB> newViews,
) {
final tabBarControllerByViewId = {...state.tabBarControllerByViewId};
for (final view in newViews) {
final controller = DatabaseTarBarController(view: view);
controller.onViewUpdated = (newView) {
add(GridTabBarEvent.viewDidUpdate(newView));
};
tabBarControllerByViewId[view.id] = controller;
}
return tabBarControllerByViewId;
}
Future<void> _createLinkedView(String name, ViewLayoutPB layoutType) async {
final viewId = state.parentView.id;
final databaseIdOrError =
await DatabaseViewBackendService(viewId: viewId).getDatabaseId();
databaseIdOrError.fold(
(databaseId) async {
final linkedViewOrError =
await ViewBackendService.createDatabaseLinkedView(
parentViewId: viewId,
databaseId: databaseId,
layoutType: layoutType,
name: name,
);
linkedViewOrError.fold(
(linkedView) {},
(err) => Log.error(err),
);
},
(r) => Log.error(r),
);
}
Future<void> _loadChildView() async {
ViewBackendService.getChildViews(viewId: state.parentView.id)
.then((viewsOrFail) {
if (isClosed) {
return;
}
viewsOrFail.fold(
(views) => add(GridTabBarEvent.didLoadChildViews(views)),
(err) => Log.error(err),
);
});
}
}
@freezed
class GridTabBarEvent with _$GridTabBarEvent {
const factory GridTabBarEvent.initial() = _Initial;
const factory GridTabBarEvent.didLoadChildViews(
List<ViewPB> childViews,
) = _DidLoadChildViews;
const factory GridTabBarEvent.selectView(String viewId) = _DidSelectView;
const factory GridTabBarEvent.createView(AddButtonAction action) =
_CreateView;
const factory GridTabBarEvent.renameView(String viewId, String newName) =
_RenameView;
const factory GridTabBarEvent.deleteView(String viewId) = _DeleteView;
const factory GridTabBarEvent.didUpdateChildViews(
ChildViewUpdatePB updatePB,
) = _DidUpdateChildViews;
const factory GridTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate;
}
@freezed
class GridTabBarState with _$GridTabBarState {
const factory GridTabBarState({
required ViewPB parentView,
required int selectedIndex,
required List<TarBar> tabBars,
required Map<String, DatabaseTarBarController> tabBarControllerByViewId,
}) = _GridTabBarState;
factory GridTabBarState.initial(ViewPB view) {
final tabBar = TarBar(view: view);
return GridTabBarState(
parentView: view,
selectedIndex: 0,
tabBars: [tabBar],
tabBarControllerByViewId: {
view.id: DatabaseTarBarController(
view: view,
)
},
);
}
}
class TarBar extends Equatable {
final ViewPB view;
final DatabaseTabBarItemBuilder _builder;
String get viewId => view.id;
DatabaseTabBarItemBuilder get builder => _builder;
ViewLayoutPB get layout => view.layout;
TarBar({
required this.view,
}) : _builder = view.tarBarItem();
@override
List<Object?> get props => [view.hashCode];
}
typedef OnViewUpdated = void Function(ViewPB newView);
typedef OnViewChildViewChanged = void Function(
ChildViewUpdatePB childViewUpdate,
);
class DatabaseTarBarController {
ViewPB view;
final DatabaseController controller;
final ViewListener viewListener;
OnViewUpdated? onViewUpdated;
OnViewChildViewChanged? onViewChildViewChanged;
DatabaseTarBarController({
required this.view,
}) : controller = DatabaseController(view: view),
viewListener = ViewListener(viewId: view.id) {
viewListener.start(
onViewChildViewsUpdated: (update) {
onViewChildViewChanged?.call(update);
},
onViewUpdated: (newView) {
view = newView;
onViewUpdated?.call(newView);
},
);
}
Future<void> dispose() async {
await viewListener.stop();
await controller.dispose();
}
}
@@ -35,7 +35,7 @@ class DatabaseViewCache {
final String viewId;
late RowCache _rowCache;
final DatabaseViewListener _databaseViewListener;
DatabaseViewCallbacks? _callbacks;
final List<DatabaseViewCallbacks> _callbacks = [];
UnmodifiableListView<RowInfo> get rowInfos => _rowCache.rowInfos;
RowCache get rowCache => _rowCache;
@@ -61,22 +61,28 @@ class DatabaseViewCache {
_rowCache.applyRowsChanged(changeset);
if (changeset.deletedRows.isNotEmpty) {
_callbacks?.onRowsDeleted?.call(changeset.deletedRows);
for (final callback in _callbacks) {
callback.onRowsDeleted?.call(changeset.deletedRows);
}
}
if (changeset.updatedRows.isNotEmpty) {
_callbacks?.onRowsUpdated?.call(
changeset.updatedRows.map((e) => e.rowId).toList(),
_rowCache.changeReason,
);
for (final callback in _callbacks) {
callback.onRowsUpdated?.call(
changeset.updatedRows.map((e) => e.rowId).toList(),
_rowCache.changeReason,
);
}
}
if (changeset.insertedRows.isNotEmpty) {
_callbacks?.onRowsCreated?.call(
changeset.insertedRows
.map((insertedRow) => insertedRow.rowMeta.id)
.toList(),
);
for (final callback in _callbacks) {
callback.onRowsCreated?.call(
changeset.insertedRows
.map((insertedRow) => insertedRow.rowMeta.id)
.toList(),
);
}
}
},
(err) => Log.error(err),
@@ -103,21 +109,25 @@ class DatabaseViewCache {
);
_rowCache.onRowsChanged(
(reason) => _callbacks?.onNumOfRowsChanged?.call(
rowInfos,
_rowCache.rowByRowId,
reason,
),
(reason) {
for (final callback in _callbacks) {
callback.onNumOfRowsChanged?.call(
rowInfos,
_rowCache.rowByRowId,
reason,
);
}
},
);
}
Future<void> dispose() async {
await _databaseViewListener.stop();
await _rowCache.dispose();
_callbacks = null;
_callbacks.clear();
}
void setListener(DatabaseViewCallbacks callbacks) {
_callbacks = callbacks;
void addListener(DatabaseViewCallbacks callbacks) {
_callbacks.add(callbacks);
}
}
@@ -28,11 +28,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId;
BoardBloc({required ViewPB view})
: databaseController = DatabaseController(
view: view,
),
super(BoardState.initial(view.id)) {
BoardBloc({
required ViewPB view,
required this.databaseController,
}) : super(BoardState.initial(view.id)) {
boardController = AppFlowyBoardController(
onMoveGroup: (
fromGroupId,
@@ -168,7 +167,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
@override
Future<void> close() async {
await databaseController.dispose();
for (final controller in groupControllers.values) {
controller.dispose();
}
@@ -235,7 +233,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
},
);
databaseController.setListener(
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onGroupChanged: onGroupChanged,
);
@@ -1,19 +1,14 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'presentation/board_page.dart';
class BoardPluginBuilder implements PluginBuilder {
@override
Plugin build(dynamic data) {
if (data is ViewPB) {
return BoardPlugin(pluginType: pluginType, view: data);
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
} else {
throw FlowyPluginException.invalidData;
}
@@ -36,55 +31,3 @@ class BoardPluginConfig implements PluginConfig {
@override
bool get creatable => true;
}
class BoardPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
BoardPlugin({
required ViewPB view,
required PluginType pluginType,
bool listenOnViewChanged = false,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
);
@override
PluginWidgetBuilder get widgetBuilder =>
BoardPluginWidgetBuilder(notifier: notifier);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class BoardPluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
BoardPluginWidgetBuilder({required this.notifier, Key? key});
ViewPB get view => notifier.view;
@override
Widget get leftBarItem => ViewLeftBarItem(view: view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(view, deletedView.index);
}
});
});
return BoardPage(key: ValueKey(view.id), view: view);
}
@override
List<NavigationItem> get navigationItems => [this];
}
@@ -3,9 +3,12 @@
import 'dart:collection';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -24,11 +27,48 @@ import '../../widgets/card/card_cell_builder.dart';
import '../../widgets/row/cell_builder.dart';
import '../application/board_bloc.dart';
import '../../widgets/card/card.dart';
import 'toolbar/board_toolbar.dart';
import 'toolbar/board_setting_bar.dart';
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
@override
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
) {
return BoardPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return BoardSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
);
}
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return SizedBox.fromSize();
}
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
}
class BoardPage extends StatelessWidget {
final DatabaseController databaseController;
BoardPage({
required this.view,
required this.databaseController,
Key? key,
this.onEditStateChanged,
}) : super(key: ValueKey(view.id));
@@ -41,8 +81,10 @@ class BoardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
BoardBloc(view: view)..add(const BoardEvent.initial()),
create: (context) => BoardBloc(
view: view,
databaseController: databaseController,
)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (p, c) => p.loadingState != c.loadingState,
builder: (context, state) {
@@ -54,7 +96,7 @@ class BoardPage extends StatelessWidget {
(_) => BoardContent(
onEditStateChanged: onEditStateChanged,
),
(err) => FlowyErrorPage(err.toString()),
(err) => FlowyErrorPage.message(err.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),),
);
},
);
@@ -110,14 +152,9 @@ class _BoardContentState extends State<BoardContent> {
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
builder: (context, state) {
final column = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: column,
padding: GridSize.contentInsets,
child: _buildBoard(context),
);
},
),
@@ -125,22 +162,20 @@ class _BoardContentState extends State<BoardContent> {
}
Widget _buildBoard(BuildContext context) {
return Expanded(
child: AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: ScrollController(),
controller: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
),
return AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: ScrollController(),
controller: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
),
);
}
@@ -335,17 +370,6 @@ class _BoardContentState extends State<BoardContent> {
}
}
class _ToolbarBlocAdaptor extends StatelessWidget {
const _ToolbarBlocAdaptor({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) => const BoardToolbar(),
);
}
}
Widget? _buildHeaderIcon(GroupData customData) {
Widget? widget;
switch (customData.fieldType) {
@@ -1,10 +1,11 @@
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardToolbar extends StatelessWidget {
const BoardToolbar({
class BoardSettingBar extends StatelessWidget {
final DatabaseController databaseController;
const BoardSettingBar({
required this.databaseController,
Key? key,
}) : super(key: key);
@@ -15,9 +16,7 @@ class BoardToolbar extends StatelessWidget {
child: Row(
children: [
const Spacer(),
SettingButton(
databaseController: context.read<BoardBloc>().databaseController,
),
SettingButton(databaseController: databaseController),
],
),
);
@@ -27,9 +27,8 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
CellCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
CalendarBloc({required ViewPB view})
: databaseController = DatabaseController(view: view),
super(CalendarState.initial()) {
CalendarBloc({required ViewPB view, required this.databaseController})
: super(CalendarState.initial()) {
on<CalendarEvent>(
(event, emit) async {
await event.when(
@@ -39,6 +38,12 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
_loadAllEvents();
},
didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) {
// If the field id changed, reload all events
state.settings.fold(() => null, (oldSetting) {
if (oldSetting.fieldId != settings.fieldId) {
_loadAllEvents();
}
});
emit(state.copyWith(settings: Some(settings)));
},
didReceiveDatabaseUpdate: (DatabasePB database) {
@@ -53,10 +58,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
),
);
},
didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) {
_loadAllEvents();
emit(state.copyWith(settings: Some(layoutSettings)));
},
createEvent: (DateTime date, String title) async {
await _createEvent(date, title);
},
@@ -105,12 +106,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
);
}
@override
Future<void> close() async {
await databaseController.dispose();
return super.close();
}
FieldInfo? _getCalendarFieldInfo(String fieldId) {
final fieldInfos = databaseController.fieldController.fieldInfos;
final index = fieldInfos.indexWhere(
@@ -149,7 +144,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
Future<void> _createEvent(DateTime date, String title) async {
return state.settings.fold(
() => null,
() {
Log.warn('Calendar settings not found');
},
(settings) async {
final dateField = _getCalendarFieldInfo(settings.fieldId);
final titleField = _getTitleFieldInfo();
@@ -207,7 +204,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
Future<void> _updateCalendarLayoutSetting(
CalendarLayoutSettingPB layoutSetting,
) async {
return databaseController.updateCalenderLayoutSetting(layoutSetting);
return databaseController.updateLayoutSetting(layoutSetting);
}
Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {
@@ -333,14 +330,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
onLoadLayout: _didReceiveLayoutSetting,
);
final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
onCalendarLayoutChanged: _didReceiveNewLayoutField,
);
databaseController.setListener(
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged,
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
);
}
@@ -353,13 +345,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
}
}
void _didReceiveNewLayoutField(DatabaseLayoutSettingPB layoutSetting) {
if (layoutSetting.hasCalendar()) {
if (isClosed) return;
add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
}
}
bool isEventDayChanged(CalendarEventData<CalendarDayEvent> event) {
final index = state.allEvents.indexWhere(
(element) => element.event!.eventId == event.event!.eventId,
@@ -426,10 +411,6 @@ class CalendarEvent with _$CalendarEvent {
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
_ReceiveDatabaseUpdate;
const factory CalendarEvent.didReceiveNewLayoutField(
CalendarLayoutSettingPB layoutSettings,
) = _DidReceiveNewLayoutField;
}
@freezed
@@ -0,0 +1,167 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../application/database_controller.dart';
import '../../application/row/row_cache.dart';
part 'unschedule_event_bloc.freezed.dart';
class UnscheduleEventsBloc
extends Bloc<UnscheduleEventsEvent, UnscheduleEventsState> {
final DatabaseController databaseController;
Map<String, FieldInfo> fieldInfoByFieldId = {};
// Getters
String get viewId => databaseController.viewId;
FieldController get fieldController => databaseController.fieldController;
CellCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
UnscheduleEventsBloc({
required this.databaseController,
}) : super(UnscheduleEventsState.initial()) {
on<UnscheduleEventsEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
_loadAllEvents();
},
didLoadAllEvents: (events) {
emit(
state.copyWith(
allEvents: events,
unscheduleEvents:
events.where((element) => !element.isScheduled).toList(),
),
);
},
didDeleteEvents: (List<RowId> deletedRowIds) {
final events = [...state.allEvents];
events.retainWhere(
(element) => !deletedRowIds.contains(element.rowMeta.id),
);
emit(
state.copyWith(
allEvents: events,
unscheduleEvents:
events.where((element) => !element.isScheduled).toList(),
),
);
},
didReceiveEvent: (CalendarEventPB event) {
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
),
);
},
);
},
);
}
Future<CalendarEventPB?> _loadEvent(
RowId rowId,
) async {
final payload = RowIdPB(viewId: viewId, rowId: rowId);
return DatabaseEventGetCalendarEvent(payload).send().then(
(result) => result.fold(
(eventPB) => eventPB,
(r) {
Log.error(r);
return null;
},
),
);
}
Future<void> _loadAllEvents() async {
final payload = CalendarEventRequestPB.create()..viewId = viewId;
DatabaseEventGetAllCalendarEvents(payload).send().then((result) {
result.fold(
(events) {
if (!isClosed) {
add(UnscheduleEventsEvent.didLoadAllEvents(events.items));
}
},
(r) => Log.error(r),
);
});
}
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
onRowsCreated: (rowIds) async {
if (isClosed) {
return;
}
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null && !isClosed) {
add(UnscheduleEventsEvent.didReceiveEvent(event));
}
}
},
onRowsDeleted: (rowIds) {
if (isClosed) {
return;
}
add(UnscheduleEventsEvent.didDeleteEvents(rowIds));
},
onRowsUpdated: (rowIds, reason) async {
if (isClosed) {
return;
}
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null) {
add(UnscheduleEventsEvent.didDeleteEvents([id]));
add(UnscheduleEventsEvent.didReceiveEvent(event));
}
}
},
);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
}
}
@freezed
class UnscheduleEventsEvent with _$UnscheduleEventsEvent {
const factory UnscheduleEventsEvent.initial() = _InitialCalendar;
// Called after loading all the current evnets
const factory UnscheduleEventsEvent.didLoadAllEvents(
List<CalendarEventPB> events,
) = _ReceiveUnscheduleEventsEvents;
const factory UnscheduleEventsEvent.didDeleteEvents(List<RowId> rowIds) =
_DidDeleteEvents;
const factory UnscheduleEventsEvent.didReceiveEvent(
CalendarEventPB event,
) = _DidReceiveEvent;
}
@freezed
class UnscheduleEventsState with _$UnscheduleEventsState {
const factory UnscheduleEventsState({
required Option<DatabasePB> database,
required List<CalendarEventPB> allEvents,
required List<CalendarEventPB> unscheduleEvents,
}) = _UnscheduleEventsState;
factory UnscheduleEventsState.initial() => UnscheduleEventsState(
database: none(),
allEvents: [],
unscheduleEvents: [],
);
}
@@ -1,19 +1,14 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import '../../util.dart';
import 'presentation/calendar_page.dart';
class CalendarPluginBuilder extends PluginBuilder {
@override
Plugin build(dynamic data) {
if (data is ViewPB) {
return CalendarPlugin(pluginType: pluginType, view: data);
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
} else {
throw FlowyPluginException.invalidData;
}
@@ -36,55 +31,3 @@ class CalendarPluginConfig implements PluginConfig {
@override
bool get creatable => true;
}
class CalendarPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
CalendarPlugin({
required ViewPB view,
required PluginType pluginType,
bool listenOnViewChanged = false,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
);
@override
PluginWidgetBuilder get widgetBuilder =>
CalendarPluginWidgetBuilder(notifier: notifier);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class CalendarPluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
CalendarPluginWidgetBuilder({required this.notifier, Key? key});
ViewPB get view => notifier.view;
@override
Widget get leftBarItem => ViewLeftBarItem(view: view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(view, deletedView.index);
}
});
});
return CalendarPage(key: ValueKey(view.id), view: view);
}
@override
List<NavigationItem> get navigationItems => [this];
}
@@ -113,7 +113,7 @@ class CalendarDayCard extends StatelessWidget {
.add(CalendarEvent.moveEvent(event, date));
},
),
_NewEventButton(onCreate: () => onCreateEvent(date)),
NewEventButton(onCreate: () => onCreateEvent(date)),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
@@ -160,9 +160,10 @@ class _Header extends StatelessWidget {
}
}
class _NewEventButton extends StatelessWidget {
@visibleForTesting
class NewEventButton extends StatelessWidget {
final VoidCallback onCreate;
const _NewEventButton({required this.onCreate, Key? key}) : super(key: key);
const NewEventButton({required this.onCreate, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -254,7 +255,7 @@ class _EventList extends StatelessWidget {
Widget build(BuildContext context) {
return Flexible(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => _EventCard(
itemBuilder: (BuildContext context, int index) => EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
@@ -270,13 +271,14 @@ class _EventList extends StatelessWidget {
}
}
class _EventCard extends StatelessWidget {
@visibleForTesting
class EventCard extends StatelessWidget {
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
const _EventCard({
const EventCard({
required this.event,
required this.viewId,
required this.rowCache,
@@ -309,7 +311,7 @@ class _EventCard extends StatelessWidget {
cellBuilder: cellBuilder,
openCard: (context) => showEventDetails(
context: context,
event: event,
event: event.event,
viewId: viewId,
rowCache: rowCache,
),
@@ -1,5 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -15,11 +19,51 @@ import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_toolbar.dart';
import 'toolbar/calendar_setting_bar.dart';
class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
@override
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
) {
return CalendarPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return CalendarSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
);
}
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return SizedBox.fromSize();
}
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
}
class CalendarPage extends StatefulWidget {
final ViewPB view;
const CalendarPage({required this.view, super.key});
final DatabaseController databaseController;
const CalendarPage({
required this.view,
required this.databaseController,
super.key,
});
@override
State<CalendarPage> createState() => _CalendarPageState();
@@ -33,8 +77,10 @@ class _CalendarPageState extends State<CalendarPage> {
@override
void initState() {
_calendarState = GlobalKey<MonthViewState>();
_calendarBloc = CalendarBloc(view: widget.view)
..add(const CalendarEvent.initial());
_calendarBloc = CalendarBloc(
view: widget.view,
databaseController: widget.databaseController,
)..add(const CalendarEvent.initial());
super.initState();
}
@@ -79,7 +125,7 @@ class _CalendarPageState extends State<CalendarPage> {
if (state.editingEvent != null) {
showEventDetails(
context: context,
event: state.editingEvent!.event!,
event: state.editingEvent!.event!.event,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
@@ -115,8 +161,6 @@ class _CalendarPageState extends State<CalendarPage> {
builder: (context, state) {
return Column(
children: [
// const _ToolbarBlocAdaptor(),
const CalendarToolbar(),
_buildCalendar(
_eventController,
state.settings
@@ -133,15 +177,18 @@ class _CalendarPageState extends State<CalendarPage> {
Widget _buildCalendar(EventController eventController, int firstDayOfWeek) {
return Expanded(
child: MonthView(
key: _calendarState,
controller: _eventController,
cellAspectRatio: .6,
startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
cellBuilder: _calendarDayBuilder,
child: Padding(
padding: GridSize.contentInsets,
child: MonthView(
key: _calendarState,
controller: _eventController,
cellAspectRatio: .6,
startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
cellBuilder: _calendarDayBuilder,
),
),
);
}
@@ -238,12 +285,12 @@ class _CalendarPageState extends State<CalendarPage> {
void showEventDetails({
required BuildContext context,
required CalendarDayEvent event,
required CalendarEventPB event,
required String viewId,
required RowCache rowCache,
}) {
final dataController = RowController(
rowMeta: event.event.rowMeta,
rowMeta: event.rowMeta,
viewId: viewId,
rowCache: rowCache,
);
@@ -351,21 +351,16 @@ class FirstDayOfWeek extends StatelessWidget {
final symbols =
DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
// starts from sunday
final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
final index = entry.key;
final string = entry.value;
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(string),
onTap: () {
onUpdated(index);
popoverMutex.close();
},
rightIcon: firstDayOfWeek == index
? const FlowySvg(name: 'grid/checkmark')
: null,
),
const len = 2;
final items = symbols.WEEKDAYS.take(len).indexed.map((entry) {
return StartFromButton(
title: entry.$2,
dayIndex: entry.$1,
isSelected: firstDayOfWeek == entry.$1,
onTap: (index) {
onUpdated(index);
popoverMutex.close();
},
);
}).toList();
@@ -376,7 +371,7 @@ class FirstDayOfWeek extends StatelessWidget {
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: 2,
itemCount: len,
),
);
},
@@ -426,3 +421,29 @@ enum CalendarLayoutSettingAction {
showWeekNumber,
showTimeLine,
}
class StartFromButton extends StatelessWidget {
final int dayIndex;
final String title;
final bool isSelected;
final void Function(int) onTap;
const StartFromButton({
required this.title,
required this.dayIndex,
required this.onTap,
required this.isSelected,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(title),
onTap: () => onTap(dayIndex),
rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null,
),
);
}
}
@@ -0,0 +1,178 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarSettingBar extends StatelessWidget {
final DatabaseController databaseController;
const CalendarSettingBar({
required this.databaseController,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
UnscheduleEventsButton(databaseController: databaseController),
SettingButton(
databaseController: databaseController,
),
],
),
);
}
}
class UnscheduleEventsButton extends StatefulWidget {
final DatabaseController databaseController;
const UnscheduleEventsButton({
required this.databaseController,
Key? key,
}) : super(key: key);
@override
State<UnscheduleEventsButton> createState() => _UnscheduleEventsButtonState();
}
class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
late final PopoverController _popoverController;
late final UnscheduleEventsBloc _bloc;
@override
void initState() {
super.initState();
_bloc = UnscheduleEventsBloc(databaseController: widget.databaseController)
..add(const UnscheduleEventsEvent.initial());
_popoverController = PopoverController();
}
@override
dispose() {
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
child: BlocProvider.value(
value: _bloc,
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
buildWhen: (previous, current) =>
previous.unscheduleEvents.length !=
current.unscheduleEvents.length,
builder: (context, state) {
return FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
);
},
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: _bloc.viewId,
rowCache: _bloc.rowCache,
controller: _popoverController,
unscheduleEvents: _bloc.state.unscheduleEvents,
);
},
);
}
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final PopoverController controller;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
required this.controller,
required this.unscheduleEvents,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.calendar_settings_clickToAdd.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
const VSpace(6),
...unscheduleEvents.map(
(e) => UnscheduledEventCell(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e,
viewId: viewId,
rowCache: rowCache,
);
controller.close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
}
}
class UnscheduledEventCell extends StatelessWidget {
final CalendarEventPB event;
final VoidCallback onPressed;
const UnscheduledEventCell({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
),
onTap: onPressed,
),
);
}
}
@@ -1,136 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.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 '../../application/calendar_bloc.dart';
class CalendarToolbar extends StatelessWidget {
const CalendarToolbar({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const _UnscheduleEventsButton(),
SettingButton(
databaseController: context.read<CalendarBloc>().databaseController,
),
],
),
);
}
}
class _UnscheduleEventsButton extends StatefulWidget {
const _UnscheduleEventsButton({Key? key}) : super(key: key);
@override
State<_UnscheduleEventsButton> createState() =>
_UnscheduleEventsButtonState();
}
class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
late final PopoverController _controller;
@override
void initState() {
super.initState();
_controller = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
final unscheduledEvents = state.allEvents
.where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0))
.toList();
final viewId = context.read<CalendarBloc>().viewId;
final rowCache = context.read<CalendarBloc>().rowCache;
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _controller,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
child: FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
),
popupBuilder: (context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
// LocaleKeys.calendar_settings_noDateHint.tr(),
LocaleKeys.calendar_settings_clickToAdd.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
const VSpace(6),
...unscheduledEvents.map(
(e) => _UnscheduledEventItem(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e.event!,
viewId: viewId,
rowCache: rowCache,
);
_controller.close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
);
},
);
}
}
class _UnscheduledEventItem extends StatelessWidget {
final CalendarEventData<CalendarDayEvent> event;
final VoidCallback onPressed;
const _UnscheduledEventItem({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
),
onTap: onPressed,
),
);
}
}
@@ -3,17 +3,17 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'grid_accessory_bloc.freezed.dart';
class GridAccessoryMenuBloc
extends Bloc<GridAccessoryMenuEvent, GridAccessoryMenuState> {
class DatabaseViewSettingExtensionBloc extends Bloc<
DatabaseViewSettingExtensionEvent, DatabaseViewSettingExtensionState> {
final String viewId;
GridAccessoryMenuBloc({required this.viewId})
DatabaseViewSettingExtensionBloc({required this.viewId})
: super(
GridAccessoryMenuState.initial(
DatabaseViewSettingExtensionState.initial(
viewId,
),
) {
on<GridAccessoryMenuEvent>(
on<DatabaseViewSettingExtensionEvent>(
(event, emit) async {
event.when(
initial: () {},
@@ -27,22 +27,25 @@ class GridAccessoryMenuBloc
}
@freezed
class GridAccessoryMenuEvent with _$GridAccessoryMenuEvent {
const factory GridAccessoryMenuEvent.initial() = _Initial;
const factory GridAccessoryMenuEvent.toggleMenu() = _MenuVisibleChange;
class DatabaseViewSettingExtensionEvent
with _$DatabaseViewSettingExtensionEvent {
const factory DatabaseViewSettingExtensionEvent.initial() = _Initial;
const factory DatabaseViewSettingExtensionEvent.toggleMenu() =
_MenuVisibleChange;
}
@freezed
class GridAccessoryMenuState with _$GridAccessoryMenuState {
const factory GridAccessoryMenuState({
class DatabaseViewSettingExtensionState
with _$DatabaseViewSettingExtensionState {
const factory DatabaseViewSettingExtensionState({
required String viewId,
required bool isVisible,
}) = _GridAccessoryMenuState;
}) = _DatabaseViewSettingExtensionState;
factory GridAccessoryMenuState.initial(
factory DatabaseViewSettingExtensionState.initial(
String viewId,
) =>
GridAccessoryMenuState(
DatabaseViewSettingExtensionState(
viewId: viewId,
isVisible: false,
);
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -65,17 +67,27 @@ class GridBloc extends Bloc<GridEvent, GridState> {
),
);
},
didReceveFilters: (List<FilterInfo> filters) {
emit(
state.copyWith(
reorderable: filters.isEmpty && state.sorts.isEmpty,
filters: filters,
),
);
},
didReceveSorts: (List<SortInfo> sorts) {
emit(
state.copyWith(
reorderable: sorts.isEmpty && state.filters.isEmpty,
sorts: sorts,
),
);
},
);
},
);
}
@override
Future<void> close() async {
await databaseController.dispose();
return super.close();
}
RowCache getRowCache(RowId rowId) {
return databaseController.rowCache;
}
@@ -93,17 +105,29 @@ class GridBloc extends Bloc<GridEvent, GridState> {
}
},
onRowsUpdated: (rows, reason) {
add(
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
);
if (!isClosed) {
add(
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
);
}
},
onFieldsChanged: (fields) {
if (!isClosed) {
add(GridEvent.didReceiveFieldUpdate(fields));
}
},
onFiltersChanged: (filters) {
if (!isClosed) {
add(GridEvent.didReceveFilters(filters));
}
},
onSortsChanged: (sorts) {
if (!isClosed) {
add(GridEvent.didReceveSorts(sorts));
}
},
);
databaseController.setListener(onDatabaseChanged: onDatabaseChanged);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
}
Future<void> _openGrid(Emitter<GridState> emit) async {
@@ -138,6 +162,11 @@ class GridEvent with _$GridEvent {
const factory GridEvent.didReceiveGridUpdate(
DatabasePB grid,
) = _DidReceiveGridUpdate;
const factory GridEvent.didReceveFilters(List<FilterInfo> filters) =
_DidReceiveFilters;
const factory GridEvent.didReceveSorts(List<SortInfo> sorts) =
_DidReceiveSorts;
}
@freezed
@@ -149,7 +178,10 @@ class GridState with _$GridState {
required List<RowInfo> rowInfos,
required int rowCount,
required GridLoadingState loadingState,
required bool reorderable,
required RowsChangedReason reason,
required List<SortInfo> sorts,
required List<FilterInfo> filters,
}) = _GridState;
factory GridState.initial(String viewId) => GridState(
@@ -158,8 +190,11 @@ class GridState with _$GridState {
rowCount: 0,
grid: none(),
viewId: viewId,
reorderable: true,
loadingState: const _Loading(),
reason: const InitialListState(),
filters: [],
sorts: [],
);
}
@@ -1,19 +1,14 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import 'presentation/grid_page.dart';
class GridPluginBuilder implements PluginBuilder {
@override
Plugin build(dynamic data) {
if (data is ViewPB) {
return GridPlugin(pluginType: pluginType, view: data);
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
} else {
throw FlowyPluginException.invalidData;
}
@@ -36,55 +31,3 @@ class GridPluginConfig implements PluginConfig {
@override
bool get creatable => true;
}
class GridPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
GridPlugin({
required ViewPB view,
required PluginType pluginType,
bool listenOnViewChanged = false,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
);
@override
PluginWidgetBuilder get widgetBuilder =>
GridPluginWidgetBuilder(notifier: notifier);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class GridPluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
ViewPB get view => notifier.view;
GridPluginWidgetBuilder({required this.notifier, Key? key});
@override
Widget get leftBarItem => ViewLeftBarItem(view: view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(view, deletedView.index);
}
});
});
return GridPage(key: ValueKey(view.id), view: view);
}
@override
List<NavigationItem> get navigationItems => [this];
}
@@ -1,5 +1,9 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -15,32 +19,82 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../../application/field/field_controller.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_data_controller.dart';
import '../../application/setting/setting_bloc.dart';
import '../application/filter/filter_menu_bloc.dart';
import '../application/grid_bloc.dart';
import '../../application/database_controller.dart';
import '../application/sort/sort_menu_bloc.dart';
import 'grid_scroll.dart';
import '../../tar_bar/tab_bar_view.dart';
import 'layout/layout.dart';
import 'layout/sizes.dart';
import 'widgets/accessory_menu.dart';
import 'widgets/row/row.dart';
import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart';
import '../../widgets/row/row_detail.dart';
import 'widgets/shortcuts.dart';
import 'widgets/toolbar/grid_toolbar.dart';
class ToggleExtensionNotifier extends ChangeNotifier {
bool _isToggled = false;
get isToggled => _isToggled;
void toggle() {
_isToggled = !_isToggled;
notifyListeners();
}
}
class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
) {
return GridPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return GridSettingBar(
key: _makeValueKey(controller),
controller: controller,
toggleExtension: _toggleExtension,
);
}
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return DatabaseViewSettingExtension(
key: _makeValueKey(controller),
viewId: controller.viewId,
databaseController: controller,
toggleExtension: _toggleExtension,
);
}
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
}
class GridPage extends StatefulWidget {
GridPage({
final DatabaseController databaseController;
const GridPage({
required this.view,
required this.databaseController,
this.onDeleted,
Key? key,
}) : databaseController = DatabaseController(view: view),
super(key: key);
}) : super(key: key);
final ViewPB view;
final DatabaseController databaseController;
final VoidCallback? onDeleted;
@override
@@ -48,6 +102,11 @@ class GridPage extends StatefulWidget {
}
class _GridPageState extends State<GridPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@@ -58,21 +117,6 @@ class _GridPageState extends State<GridPage> {
databaseController: widget.databaseController,
)..add(const GridEvent.initial()),
),
BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: widget.view.id,
fieldController: widget.databaseController.fieldController,
)..add(const GridFilterMenuEvent.initial()),
),
BlocProvider<SortMenuBloc>(
create: (context) => SortMenuBloc(
viewId: widget.view.id,
fieldController: widget.databaseController.fieldController,
)..add(const SortMenuEvent.initial()),
),
BlocProvider<DatabaseSettingBloc>(
create: (context) => DatabaseSettingBloc(viewId: widget.view.id),
),
],
child: BlocBuilder<GridBloc, GridState>(
builder: (context, state) {
@@ -81,11 +125,12 @@ class _GridPageState extends State<GridPage> {
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.successOrFail.fold(
(_) => GridShortcuts(
child: FlowyGrid(
viewId: widget.view.id,
),
child: GridPageContent(view: widget.view),
),
(err) => FlowyErrorPage.message(
err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
(err) => FlowyErrorPage(err.toString()),
),
);
},
@@ -94,18 +139,18 @@ class _GridPageState extends State<GridPage> {
}
}
class FlowyGrid extends StatefulWidget {
final String viewId;
const FlowyGrid({
required this.viewId,
class GridPageContent extends StatefulWidget {
final ViewPB view;
const GridPageContent({
required this.view,
super.key,
});
@override
State<FlowyGrid> createState() => _FlowyGridState();
State<GridPageContent> createState() => _GridPageContentState();
}
class _FlowyGridState extends State<FlowyGrid> {
class _GridPageContentState extends State<GridPageContent> {
final _scrollController = GridScrollController(
scrollGroupController: LinkedScrollControllerGroup(),
);
@@ -129,115 +174,148 @@ class _FlowyGridState extends State<FlowyGrid> {
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final contentWidth = GridLayout.headerWidth(state.fields.value);
final child = _WrapScrollView(
scrollController: _scrollController,
contentWidth: contentWidth,
child: _GridRows(
viewId: widget.viewId,
verticalScrollController: _scrollController.verticalController,
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const GridToolbar(),
GridAccessoryMenu(viewId: state.viewId),
_gridHeader(context, state.viewId),
Flexible(child: child),
const _RowCountBadge(),
_GridHeader(headerScrollController: headerScrollController),
_GridRows(
viewId: state.viewId,
contentWidth: contentWidth,
scrollController: _scrollController,
),
const _GridFooter(),
],
);
},
);
}
}
Widget _gridHeader(BuildContext context, String viewId) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return GridHeaderSliverAdaptor(
viewId: viewId,
fieldController: fieldController,
anchorScrollController: headerScrollController,
class _GridHeader extends StatelessWidget {
final ScrollController headerScrollController;
const _GridHeader({required this.headerScrollController});
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
builder: (context, state) {
return GridHeaderSliverAdaptor(
viewId: state.viewId,
fieldController:
context.read<GridBloc>().databaseController.fieldController,
anchorScrollController: headerScrollController,
);
},
);
}
}
class _GridRows extends StatelessWidget {
final String viewId;
final double contentWidth;
final GridScrollController scrollController;
const _GridRows({
required this.viewId,
required this.verticalScrollController,
required this.contentWidth,
required this.scrollController,
});
final ScrollController verticalScrollController;
@override
Widget build(BuildContext context) {
final filterState = context.watch<GridFilterMenuBloc>().state;
final sortState = context.watch<SortMenuBloc>().state;
return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => current.reason.maybeWhen(
reorderRows: () => true,
reorderSingleRow: (reorderRow, rowInfo) => true,
delete: (item) => true,
insert: (item) => true,
orElse: () => false,
),
builder: (context, state) {
final rowInfos = state.rowInfos;
final behavior = ScrollConfiguration.of(context).copyWith(
scrollbars: false,
);
return ScrollConfiguration(
behavior: behavior,
child: ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: verticalScrollController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
index: index,
isSortEnabled: sortState.sortInfos.isNotEmpty,
isFilterEnabled: filterState.filters.isNotEmpty,
);
}
return const _GridFooter(key: Key('gridFooter'));
},
return Flexible(
child: _WrapScrollView(
scrollController: scrollController,
contentWidth: contentWidth,
child: BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => current.reason.maybeWhen(
reorderRows: () => true,
reorderSingleRow: (reorderRow, rowInfo) => true,
delete: (item) => true,
insert: (item) => true,
orElse: () => false,
),
);
},
builder: (context, state) {
final rowInfos = state.rowInfos;
final behavior = ScrollConfiguration.of(context).copyWith(
scrollbars: false,
);
return ScrollConfiguration(
behavior: behavior,
child: _renderList(context, state, rowInfos),
);
},
),
),
);
}
Widget _renderList(
BuildContext context,
GridState state,
List<RowInfo> rowInfos,
) {
if (Platform.isWindows) {
// Workaround: On Windows, the focusing of the text cell is not working
// properly when the list is reorderable. So using the ListView instead.
return ListView.builder(
controller: scrollController.verticalController,
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: false,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
} else {
return ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
}
}
Widget _renderRow(
BuildContext context,
RowId rowId, {
int? index,
bool isSortEnabled = false,
bool isFilterEnabled = false,
required bool isDraggable,
Animation<double>? animation,
}) {
final rowCache = context.read<GridBloc>().getRowCache(rowId);
@@ -259,7 +337,7 @@ class _GridRows extends StatelessWidget {
rowId: rowId,
viewId: viewId,
index: index,
isDraggable: !isSortEnabled && !isFilterEnabled,
isDraggable: isDraggable,
dataController: dataController,
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
openDetailPage: (context, cellBuilder) {
@@ -314,22 +392,6 @@ class _GridRows extends StatelessWidget {
}
}
class _GridFooter extends StatelessWidget {
const _GridFooter({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: GridSize.footerContentInsets,
height: GridSize.footerHeight,
margin: const EdgeInsets.only(bottom: 200),
child: const GridAddRowButton(),
);
}
}
class _WrapScrollView extends StatelessWidget {
const _WrapScrollView({
required this.contentWidth,
@@ -360,8 +422,8 @@ class _WrapScrollView extends StatelessWidget {
}
}
class _RowCountBadge extends StatelessWidget {
const _RowCountBadge();
class _GridFooter extends StatelessWidget {
const _GridFooter();
@override
Widget build(BuildContext context) {
@@ -369,7 +431,7 @@ class _RowCountBadge extends StatelessWidget {
selector: (state) => state.rowCount,
builder: (context, rowCount) {
return Padding(
padding: GridSize.footerContentInsets,
padding: GridSize.contentInsets,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
@@ -36,4 +36,11 @@ class GridSize {
GridSize.headerContainerPadding,
GridSize.headerContainerPadding,
);
static EdgeInsets get contentInsets => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding,
GridSize.headerContainerPadding,
GridSize.leadingHeaderPadding,
GridSize.headerContainerPadding,
);
}
@@ -1,90 +0,0 @@
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../layout/sizes.dart';
import 'filter/filter_menu.dart';
import 'sort/sort_menu.dart';
class GridAccessoryMenu extends StatelessWidget {
final String viewId;
const GridAccessoryMenu({required this.viewId, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => GridAccessoryMenuBloc(viewId: viewId),
child: MultiBlocListener(
listeners: [
BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => context
.read<GridAccessoryMenuBloc>()
.add(const GridAccessoryMenuEvent.toggleMenu()),
),
BlocListener<SortMenuBloc, SortMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => context
.read<GridAccessoryMenuBloc>()
.add(const GridAccessoryMenuEvent.toggleMenu()),
),
],
child: BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
builder: (context, state) {
if (state.isVisible) {
return const _AccessoryMenu();
} else {
return const SizedBox();
}
},
),
),
);
}
}
class _AccessoryMenu extends StatelessWidget {
const _AccessoryMenu({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
builder: (context, state) {
return _wrapPadding(
Column(
children: [
Divider(
height: 1.0,
color: AFThemeExtension.of(context).toggleOffFill,
),
const VSpace(6),
const IntrinsicHeight(
child: Row(
children: [
SortMenu(),
HSpace(6),
FilterMenu(),
],
),
),
],
),
);
},
);
}
Widget _wrapPadding(Widget child) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
vertical: 6,
),
child: child,
);
}
}
@@ -109,7 +109,7 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
children: [
FlowyText(state.filterInfo.fieldInfo.name),
const HSpace(4),
CheckboxFilterConditionPBList(
CheckboxFilterConditionList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
@@ -137,11 +137,11 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
}
}
class CheckboxFilterConditionPBList extends StatelessWidget {
class CheckboxFilterConditionList extends StatelessWidget {
final FilterInfo filterInfo;
final PopoverMutex popoverMutex;
final Function(CheckboxFilterConditionPB) onCondition;
const CheckboxFilterConditionPBList({
const CheckboxFilterConditionList({
required this.filterInfo,
required this.popoverMutex,
required this.onCondition,
@@ -93,7 +93,7 @@ class ChecklistState extends State<ChecklistFilterEditor> {
children: [
FlowyText(state.filterInfo.fieldInfo.name),
const HSpace(4),
ChecklistFilterConditionPBList(
ChecklistFilterConditionList(
filterInfo: state.filterInfo,
),
const Spacer(),
@@ -118,9 +118,9 @@ class ChecklistState extends State<ChecklistFilterEditor> {
}
}
class ChecklistFilterConditionPBList extends StatelessWidget {
class ChecklistFilterConditionList extends StatelessWidget {
final FilterInfo filterInfo;
const ChecklistFilterConditionPBList({
const ChecklistFilterConditionList({
required this.filterInfo,
Key? key,
}) : super(key: key);
@@ -2,7 +2,6 @@ import 'package:appflowy/plugins/database_view/grid/application/filter/select_op
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.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:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter/material.dart';
@@ -58,16 +57,16 @@ class SelectOptionFilterList extends StatelessWidget {
SelectOptionFilterListState>(
builder: (context, state) {
return ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
controller: ScrollController(),
itemCount: state.visibleOptions.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final option = state.visibleOptions[index];
return _SelectOptionFilterCell(
return SelectOptionFilterCell(
option: option.optionPB,
isSelected: option.isSelected,
);
@@ -80,21 +79,20 @@ class SelectOptionFilterList extends StatelessWidget {
}
}
class _SelectOptionFilterCell extends StatefulWidget {
class SelectOptionFilterCell extends StatefulWidget {
final SelectOptionPB option;
final bool isSelected;
const _SelectOptionFilterCell({
const SelectOptionFilterCell({
required this.option,
required this.isSelected,
Key? key,
}) : super(key: key);
@override
State<_SelectOptionFilterCell> createState() =>
_SelectOptionFilterCellState();
State<SelectOptionFilterCell> createState() => _SelectOptionFilterCellState();
}
class _SelectOptionFilterCellState extends State<_SelectOptionFilterCell> {
class _SelectOptionFilterCellState extends State<SelectOptionFilterCell> {
@override
Widget build(BuildContext context) {
return SizedBox(
@@ -60,7 +60,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
final cells = state.creatableFields.map((fieldInfo) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: _FilterPropertyCell(
child: GridFilterPropertyCell(
fieldInfo: fieldInfo,
onTap: (fieldInfo) => createFilter(fieldInfo),
),
@@ -147,10 +147,10 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
}
}
class _FilterPropertyCell extends StatelessWidget {
class GridFilterPropertyCell extends StatelessWidget {
final FieldInfo fieldInfo;
final Function(FieldInfo) onTap;
const _FilterPropertyCell({
const GridFilterPropertyCell({
required this.fieldInfo,
required this.onTap,
Key? key,
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -12,37 +13,49 @@ import 'create_filter_list.dart';
import 'filter_menu_item.dart';
class FilterMenu extends StatelessWidget {
const FilterMenu({Key? key}) : super(key: key);
final FieldController fieldController;
const FilterMenu({
required this.fieldController,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
builder: (context, state) {
final List<Widget> children = [];
children.addAll(
state.filters
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
.toList(),
);
return BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: fieldController.viewId,
fieldController: fieldController,
)..add(
const GridFilterMenuEvent.initial(),
),
child: BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
builder: (context, state) {
final List<Widget> children = [];
children.addAll(
state.filters
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
.toList(),
);
if (state.creatableFields.isNotEmpty) {
children.add(AddFilterButton(viewId: state.viewId));
}
if (state.creatableFields.isNotEmpty) {
children.add(AddFilterButton(viewId: state.viewId));
}
return Expanded(
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 6,
runSpacing: 4,
children: children,
return Expanded(
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 6,
runSpacing: 4,
children: children,
),
),
),
],
),
);
},
],
),
);
},
),
);
}
}
@@ -1,5 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/image.dart';
@@ -27,3 +28,19 @@ class GridAddRowButton extends StatelessWidget {
);
}
}
class GridRowBottomBar extends StatelessWidget {
const GridRowBottomBar({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: GridSize.footerContentInsets,
height: GridSize.footerHeight,
margin: const EdgeInsets.only(bottom: 200),
child: const GridAddRowButton(),
);
}
}
@@ -169,17 +169,14 @@ class FieldActionCell extends StatelessWidget {
Widget build(BuildContext context) {
return FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
disable: !enable,
text: FlowyText.medium(
action.title(),
color: enable
? AFThemeExtension.of(context).textColor
: Theme.of(context).disabledColor,
),
onTap: () {
if (enable) {
action.run(context, fieldInfo);
}
},
onTap: () => action.run(context, fieldInfo),
leftIcon: svgWidget(
action.iconName(),
color: enable
@@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show none;
import 'package:easy_localization/easy_localization.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';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
@@ -232,15 +233,16 @@ class _DeleteFieldButton extends StatelessWidget {
LocaleKeys.grid_field_delete.tr(),
color: enable ? null : Theme.of(context).disabledColor,
),
leftIcon: svgWidget(
'grid/delete',
color: enable ? null : Theme.of(context).disabledColor,
),
onTap: () {
if (enable) onDeleted?.call();
},
onHover: (_) => popoverMutex.close(),
);
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: SizedBox(height: GridSize.popoverItemHeight, child: button),
);
return SizedBox(height: GridSize.popoverItemHeight, child: button);
},
);
}
@@ -265,13 +267,11 @@ class _HideFieldButton extends StatelessWidget {
text: FlowyText.medium(
LocaleKeys.grid_field_hide.tr(),
),
leftIcon: svgWidget('grid/hide'),
onTap: () => onHidden?.call(),
onHover: (_) => popoverMutex.close(),
);
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: SizedBox(height: GridSize.popoverItemHeight, child: button),
);
return SizedBox(height: GridSize.popoverItemHeight, child: button);
},
);
}
@@ -115,7 +115,6 @@ class SwitchFieldButton extends StatelessWidget {
text: FlowyText.medium(
bloc.state.field.fieldType.title(),
),
margin: GridSize.typeOptionContentInsets,
leftIcon: FlowySvg(name: bloc.state.field.fieldType.iconName()),
rightIcon: const FlowySvg(name: 'grid/more'),
);
@@ -93,11 +93,9 @@ class DateTypeOptionWidget extends TypeOptionWidget {
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(
buttonMargins: GridSize.typeOptionContentInsets,
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(),
),
);
}
@@ -125,10 +123,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(
timeFormat: timeFormat,
buttonMargins: GridSize.typeOptionContentInsets,
),
child: TimeFormatButton(timeFormat: timeFormat),
),
);
}
@@ -137,11 +132,9 @@ class DateTypeOptionWidget extends TypeOptionWidget {
class DateFormatButton extends StatelessWidget {
final VoidCallback? onTap;
final void Function(bool)? onHover;
final EdgeInsets? buttonMargins;
const DateFormatButton({
this.onTap,
this.onHover,
this.buttonMargins,
Key? key,
}) : super(key: key);
@@ -151,7 +144,6 @@ class DateFormatButton extends StatelessWidget {
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
margin: buttonMargins,
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(name: 'grid/more'),
@@ -164,12 +156,10 @@ class TimeFormatButton extends StatelessWidget {
final TimeFormatPB timeFormat;
final VoidCallback? onTap;
final void Function(bool)? onHover;
final EdgeInsets? buttonMargins;
const TimeFormatButton({
required this.timeFormat,
this.onTap,
this.onHover,
this.buttonMargins,
Key? key,
}) : super(key: key);
@@ -179,7 +169,6 @@ class TimeFormatButton extends StatelessWidget {
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
margin: buttonMargins,
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(name: 'grid/more'),
@@ -209,7 +209,7 @@ class _OptionCellState extends State<_OptionCell> {
offset: const Offset(8, 0),
margin: EdgeInsets.zero,
asBarrier: true,
constraints: BoxConstraints.loose(const Size(460, 460)),
constraints: BoxConstraints.loose(const Size(460, 470)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: child,
@@ -138,7 +138,6 @@ class _OptionNameTextField extends StatelessWidget {
return FlowyTextField(
autoFocus: autoFocus,
text: name,
maxLength: 30,
submitOnLeave: true,
onSubmitted: (newName) {
if (name != newName) {
@@ -59,7 +59,7 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
final cells = state.creatableFields.map((fieldInfo) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: _SortPropertyCell(
child: GridSortPropertyCell(
fieldInfo: fieldInfo,
onTap: (fieldInfo) => createSort(fieldInfo),
),
@@ -69,7 +69,7 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
final List<Widget> slivers = [
SliverPersistentHeader(
pinned: true,
delegate: _FilterTextFieldDelegate(),
delegate: _SortTextFieldDelegate(),
),
SliverToBoxAdapter(
child: ListView.separated(
@@ -109,8 +109,8 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
}
}
class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
_FilterTextFieldDelegate();
class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate {
_SortTextFieldDelegate();
double fixHeight = 46;
@@ -146,10 +146,10 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
}
}
class _SortPropertyCell extends StatelessWidget {
class GridSortPropertyCell extends StatelessWidget {
final FieldInfo fieldInfo;
final Function(FieldInfo) onTap;
const _SortPropertyCell({
const GridSortPropertyCell({
required this.fieldInfo,
required this.onTap,
Key? key,
@@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
@@ -13,12 +12,9 @@ class OrderPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final List<Widget> children = SortConditionPB.values.map((condition) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(textFromCondition(condition)),
onTap: () => onCondition(condition),
),
return OrderPannelItem(
condition: condition,
onCondition: onCondition,
);
}).toList();
@@ -33,14 +29,25 @@ class OrderPanel extends StatelessWidget {
),
);
}
}
String textFromCondition(SortConditionPB condition) {
switch (condition) {
case SortConditionPB.Ascending:
return LocaleKeys.grid_sort_ascending.tr();
case SortConditionPB.Descending:
return LocaleKeys.grid_sort_descending.tr();
}
return "";
class OrderPannelItem extends StatelessWidget {
final SortConditionPB condition;
final Function(SortConditionPB) onCondition;
const OrderPannelItem({
required this.condition,
required this.onCondition,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(condition.title),
onTap: () => onCondition(condition),
),
);
}
}
@@ -51,12 +51,12 @@ class _SortEditorState extends State<SortEditor> {
child: Column(
children: [
_SortList(popoverMutex: popoverMutex),
_AddSortButton(
DatabaseAddSortButton(
viewId: widget.viewId,
fieldController: widget.fieldController,
popoverMutex: popoverMutex,
),
_DeleteSortButton(popoverMutex: popoverMutex),
DatabaseDeleteSortButton(popoverMutex: popoverMutex),
],
),
),
@@ -79,7 +79,7 @@ class _SortList extends StatelessWidget {
.map(
(info) => Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: _SortItem(
child: DatabaseSortItem(
sortInfo: info,
popoverMutex: popoverMutex,
),
@@ -95,10 +95,10 @@ class _SortList extends StatelessWidget {
}
}
class _SortItem extends StatelessWidget {
class DatabaseSortItem extends StatelessWidget {
final SortInfo sortInfo;
final PopoverMutex popoverMutex;
const _SortItem({
const DatabaseSortItem({
required this.popoverMutex,
required this.sortInfo,
Key? key,
@@ -111,7 +111,7 @@ class _SortItem extends StatelessWidget {
editable: false,
onTap: () {},
);
final orderButton = _OrderButton(
final orderButton = DatabaseSortItemOrderButton(
sortInfo: sortInfo,
popoverMutex: popoverMutex,
);
@@ -141,9 +141,11 @@ class _SortItem extends StatelessWidget {
],
);
}
}
String textFromCondition(SortConditionPB condition) {
switch (condition) {
extension SortConditionExtension on SortConditionPB {
String get title {
switch (this) {
case SortConditionPB.Ascending:
return LocaleKeys.grid_sort_ascending.tr();
case SortConditionPB.Descending:
@@ -153,11 +155,11 @@ class _SortItem extends StatelessWidget {
}
}
class _AddSortButton extends StatefulWidget {
class DatabaseAddSortButton extends StatefulWidget {
final String viewId;
final FieldController fieldController;
final PopoverMutex popoverMutex;
const _AddSortButton({
const DatabaseAddSortButton({
required this.viewId,
required this.fieldController,
required this.popoverMutex,
@@ -165,10 +167,10 @@ class _AddSortButton extends StatefulWidget {
}) : super(key: key);
@override
State<_AddSortButton> createState() => _AddSortButtonState();
State<DatabaseAddSortButton> createState() => _DatabaseAddSortButtonState();
}
class _AddSortButtonState extends State<_AddSortButton> {
class _DatabaseAddSortButtonState extends State<DatabaseAddSortButton> {
final _popoverController = PopoverController();
@override
@@ -202,9 +204,9 @@ class _AddSortButtonState extends State<_AddSortButton> {
}
}
class _DeleteSortButton extends StatelessWidget {
class DatabaseDeleteSortButton extends StatelessWidget {
final PopoverMutex popoverMutex;
const _DeleteSortButton({required this.popoverMutex, Key? key})
const DatabaseDeleteSortButton({required this.popoverMutex, Key? key})
: super(key: key);
@override
@@ -228,20 +230,22 @@ class _DeleteSortButton extends StatelessWidget {
}
}
class _OrderButton extends StatefulWidget {
class DatabaseSortItemOrderButton extends StatefulWidget {
final SortInfo sortInfo;
final PopoverMutex popoverMutex;
const _OrderButton({
const DatabaseSortItemOrderButton({
required this.popoverMutex,
required this.sortInfo,
Key? key,
}) : super(key: key);
@override
_OrderButtonState createState() => _OrderButtonState();
State<DatabaseSortItemOrderButton> createState() =>
_DatabaseSortItemOrderButtonState();
}
class _OrderButtonState extends State<_OrderButton> {
class _DatabaseSortItemOrderButtonState
extends State<DatabaseSortItemOrderButton> {
final PopoverController popoverController = PopoverController();
@override
@@ -268,20 +272,10 @@ class _OrderButtonState extends State<_OrderButton> {
);
},
child: SortChoiceButton(
text: textFromCondition(widget.sortInfo.sortPB.condition),
text: widget.sortInfo.sortPB.condition.title,
rightIcon: arrow,
onTap: () => popoverController.show(),
),
);
}
String textFromCondition(SortConditionPB condition) {
switch (condition) {
case SortConditionPB.Ascending:
return LocaleKeys.grid_sort_ascending.tr();
case SortConditionPB.Descending:
return LocaleKeys.grid_sort_descending.tr();
}
return "";
}
}
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -13,30 +14,40 @@ import 'sort_editor.dart';
import 'sort_info.dart';
class SortMenu extends StatelessWidget {
const SortMenu({Key? key}) : super(key: key);
final FieldController fieldController;
const SortMenu({
required this.fieldController,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<SortMenuBloc, SortMenuState>(
builder: (context, state) {
if (state.sortInfos.isNotEmpty) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(340, 200)),
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
return SortEditor(
viewId: state.viewId,
fieldController: context.read<SortMenuBloc>().fieldController,
sortInfos: state.sortInfos,
);
},
child: SortChoiceChip(sortInfos: state.sortInfos),
);
} else {
return const SizedBox();
}
},
return BlocProvider<SortMenuBloc>(
create: (context) => SortMenuBloc(
viewId: fieldController.viewId,
fieldController: fieldController,
)..add(const SortMenuEvent.initial()),
child: BlocBuilder<SortMenuBloc, SortMenuState>(
builder: (context, state) {
if (state.sortInfos.isNotEmpty) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(340, 200)),
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
return SortEditor(
viewId: state.viewId,
fieldController: context.read<SortMenuBloc>().fieldController,
sortInfos: state.sortInfos,
);
},
child: SortChoiceChip(sortInfos: state.sortInfos),
);
} else {
return const SizedBox();
}
},
),
);
}
}
@@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -67,34 +66,6 @@ class _DatabaseLayoutListState extends State<DatabaseLayoutList> {
}
}
extension DatabaseLayoutExtension on DatabaseLayoutPB {
String layoutName() {
switch (this) {
case DatabaseLayoutPB.Board:
return LocaleKeys.board_menuName.tr();
case DatabaseLayoutPB.Calendar:
return LocaleKeys.calendar_menuName.tr();
case DatabaseLayoutPB.Grid:
return LocaleKeys.grid_menuName.tr();
default:
return "";
}
}
String iconName() {
switch (this) {
case DatabaseLayoutPB.Board:
return 'editor/board';
case DatabaseLayoutPB.Calendar:
return "editor/grid";
case DatabaseLayoutPB.Grid:
return "editor/grid";
default:
return "";
}
}
}
class DatabaseViewLayoutCell extends StatelessWidget {
final bool isSelected;
final DatabaseLayoutPB databaseLayout;
@@ -0,0 +1,68 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'filter_button.dart';
import 'sort_button.dart';
class GridSettingBar extends StatelessWidget {
final DatabaseController controller;
final ToggleExtensionNotifier toggleExtension;
const GridSettingBar({
required this.controller,
required this.toggleExtension,
super.key,
});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: controller.viewId,
fieldController: controller.fieldController,
)..add(const GridFilterMenuEvent.initial()),
),
BlocProvider<SortMenuBloc>(
create: (context) => SortMenuBloc(
viewId: controller.viewId,
fieldController: controller.fieldController,
)..add(const SortMenuEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
),
BlocListener<SortMenuBloc, SortMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
),
],
child: SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
const Spacer(),
const FilterButton(),
const SortButton(),
SettingButton(
databaseController: controller,
),
],
),
),
),
);
}
}
@@ -1,41 +0,0 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
import 'filter_button.dart';
import '../../../../widgets/setting/setting_button.dart';
import 'sort_button.dart';
class GridToolbarContext {
final String viewId;
final FieldController fieldController;
GridToolbarContext({
required this.viewId,
required this.fieldController,
});
}
class GridToolbar extends StatelessWidget {
const GridToolbar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
const Spacer(),
const FilterButton(),
const SortButton(),
SettingButton(
databaseController: context.read<GridBloc>().databaseController,
),
],
),
);
}
}
@@ -0,0 +1,98 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import '../application/field/field_controller.dart';
import '../grid/presentation/layout/sizes.dart';
import '../grid/presentation/widgets/filter/filter_menu.dart';
import '../grid/presentation/widgets/sort/sort_menu.dart';
class DatabaseViewSettingExtension extends StatelessWidget {
final String viewId;
final DatabaseController databaseController;
final ToggleExtensionNotifier toggleExtension;
const DatabaseViewSettingExtension({
required this.viewId,
required this.databaseController,
required this.toggleExtension,
super.key,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: toggleExtension,
child: Consumer<ToggleExtensionNotifier>(
builder: (context, value, child) {
if (value.isToggled) {
return BlocProvider(
create: (context) =>
DatabaseViewSettingExtensionBloc(viewId: viewId),
child: _DatabaseViewSettingContent(
fieldController: databaseController.fieldController,
),
);
} else {
return const SizedBox();
}
},
),
);
}
}
class _DatabaseViewSettingContent extends StatelessWidget {
final FieldController fieldController;
const _DatabaseViewSettingContent({
required this.fieldController,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<DatabaseViewSettingExtensionBloc,
DatabaseViewSettingExtensionState>(
builder: (context, state) {
final children = <Widget>[
Divider(
height: 1.0,
color: AFThemeExtension.of(context).toggleOffFill,
),
const VSpace(6),
IntrinsicHeight(
child: Row(
children: [
SortMenu(
fieldController: fieldController,
),
const HSpace(6),
FilterMenu(
fieldController: fieldController,
),
],
),
)
];
return _wrapPadding(
Column(children: children),
);
},
);
}
Widget _wrapPadding(Widget child) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
vertical: 6,
),
child: child,
);
}
}
@@ -0,0 +1,415 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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/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 '../application/database_controller.dart';
import '../grid/presentation/layout/sizes.dart';
import 'tar_bar_add_button.dart';
abstract class DatabaseTabBarItemBuilder {
const DatabaseTabBarItemBuilder();
/// Returns the content of the tab bar item. The content is shown when the tab
/// bar item is selected. It can be any kind of database view.
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
);
/// Returns the setting bar of the tab bar item. The setting bar is shown on the
/// top right conner when the tab bar item is selected.
Widget settingBar(
BuildContext context,
DatabaseController controller,
);
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
);
}
class DatabaseTabBarView extends StatefulWidget {
final ViewPB view;
const DatabaseTabBarView({
required this.view,
super.key,
});
@override
State<DatabaseTabBarView> createState() => _DatabaseTabBarViewState();
}
class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
PageController? _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(
initialPage: 0,
);
}
@override
Widget build(BuildContext context) {
return BlocProvider<GridTabBarBloc>(
create: (context) => GridTabBarBloc(view: widget.view)
..add(
const GridTabBarEvent.initial(),
),
child: MultiBlocListener(
listeners: [
BlocListener<GridTabBarBloc, GridTabBarState>(
listenWhen: (p, c) => p.selectedIndex != c.selectedIndex,
listener: (context, state) {
_pageController?.animateToPage(
state.selectedIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
},
),
],
child: Column(
children: [
Row(
children: [
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return const Flexible(
child: Padding(
padding: EdgeInsets.only(left: 50),
child: DatabaseTabBar(),
),
);
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return SizedBox(
width: 300,
child: Padding(
padding: const EdgeInsets.only(right: 50),
child: pageSettingBarFromState(state),
),
);
},
),
],
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return pageSettingBarExtensionFromState(state);
},
),
Expanded(
child: BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return PageView(
pageSnapping: false,
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
children: pageContentFromState(state),
);
},
),
),
],
),
),
);
}
List<Widget> pageContentFromState(GridTabBarState state) {
return state.tabBars.map((tabBar) {
final controller =
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tabBar.builder.content(
context,
tabBar.view,
controller,
);
}).toList();
}
Widget pageSettingBarFromState(GridTabBarState state) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBar(
context,
controller,
);
}
Widget pageSettingBarExtensionFromState(GridTabBarState state) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBarExtension(
context,
controller,
);
}
}
class DatabaseTabBarViewPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
DatabaseTabBarViewPlugin({
required ViewPB view,
required PluginType pluginType,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(view: view);
@override
PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder(
notifier: notifier,
);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
DatabasePluginWidgetBuilder({
required this.notifier,
Key? key,
});
@override
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(notifier.view, deletedView.index);
}
});
});
return DatabaseTabBarView(
key: ValueKey(notifier.view.id),
view: notifier.view,
);
}
@override
List<NavigationItem> get navigationItems => [this];
}
class DatabaseTabBar extends StatefulWidget {
const DatabaseTabBar({super.key});
@override
State<DatabaseTabBar> createState() => _DatabaseTabBarState();
}
class _DatabaseTabBarState extends State<DatabaseTabBar> {
final _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
final children = state.tabBars.indexed.map((indexed) {
final isSelected = state.selectedIndex == indexed.$1;
final tabBar = indexed.$2;
return DatabaseTabBarItem(
key: ValueKey(tabBar.viewId),
view: tabBar.view,
isSelected: isSelected,
onTap: (selectedView) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.selectView(selectedView.id),
);
},
);
}).toList();
return Row(
children: [
Flexible(
child: SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: IntrinsicWidth(
child: Row(children: children),
),
),
),
AddDatabaseViewButton(
onTap: (action) async {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.createView(action),
);
},
),
],
);
},
);
}
}
class DatabaseTabBarItem extends StatelessWidget {
final bool isSelected;
final ViewPB view;
final Function(ViewPB) onTap;
const DatabaseTabBarItem({
required this.view,
required this.isSelected,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: 80, maxWidth: 160),
child: IntrinsicWidth(
child: Column(
children: [
TabBarItemButton(
view: view,
onTap: () => onTap(view),
),
if (isSelected)
Divider(
height: 1,
thickness: 2,
color: Theme.of(context).colorScheme.secondary,
),
],
),
),
);
}
}
class TabBarItemButton extends StatelessWidget {
final ViewPB view;
final VoidCallback onTap;
const TabBarItemButton({
required this.view,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return PopoverActionList<TabBarViewAction>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: TabBarViewAction.values,
buildChild: (controller) {
return FlowyButton(
radius: Corners.s5Border,
hoverColor: AFThemeExtension.of(context).greyHover,
onTap: onTap,
onSecondaryTap: () {
controller.show();
},
text: FlowyText.medium(
view.name,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
margin: GridSize.cellContentInsets,
leftIcon: svgWidget(
view.iconName,
color: Theme.of(context).iconTheme.color,
),
);
},
onSelected: (action, controller) {
switch (action) {
case TabBarViewAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
value: view.name,
confirm: (newValue) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.renameView(view.id, newValue),
);
},
).show(context);
break;
case TabBarViewAction.delete:
NavigatorAlertDialog(
title: LocaleKeys.grid_deleteView.tr(),
confirm: () {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.deleteView(view.id),
);
},
).show(context);
break;
}
controller.close();
},
);
}
}
enum TabBarViewAction implements ActionCell {
rename,
delete;
@override
String get name {
switch (this) {
case TabBarViewAction.rename:
return LocaleKeys.disclosureAction_rename.tr();
case TabBarViewAction.delete:
return LocaleKeys.disclosureAction_delete.tr();
}
}
Widget icon(Color iconColor) {
switch (this) {
case TabBarViewAction.rename:
return const FlowySvg(name: 'editor/edit');
case TabBarViewAction.delete:
return const FlowySvg(name: 'editor/delete');
}
}
@override
Widget? leftIcon(Color iconColor) => icon(iconColor);
@override
Widget? rightIcon(Color iconColor) => null;
}
@@ -0,0 +1,156 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flutter/material.dart';
class AddDatabaseViewButton extends StatefulWidget {
final Function(AddButtonAction) onTap;
const AddDatabaseViewButton({
required this.onTap,
super.key,
});
@override
State<AddDatabaseViewButton> createState() => _AddDatabaseViewButtonState();
}
class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
final popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: popoverController,
constraints: BoxConstraints.loose(const Size(200, 400)),
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 8),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
child: FlowyIconButton(
iconPadding: const EdgeInsets.all(4),
hoverColor: AFThemeExtension.of(context).greyHover,
onPressed: () => popoverController.show(),
icon: svgWidget(
'home/add',
color: Theme.of(context).colorScheme.tertiary,
),
),
popupBuilder: (BuildContext context) {
return TarBarAddButtonAction(
onTap: (action) {
popoverController.close();
widget.onTap(action);
},
);
},
);
}
}
class TarBarAddButtonAction extends StatelessWidget {
final Function(AddButtonAction) onTap;
const TarBarAddButtonAction({
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = AddButtonAction.values.map((layout) {
return TarBarAddButtonActionCell(
action: layout,
onTap: onTap,
);
}).toList();
return ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) => cells[index],
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
padding: const EdgeInsets.symmetric(vertical: 6.0),
);
}
}
class TarBarAddButtonActionCell extends StatelessWidget {
final AddButtonAction action;
final void Function(AddButtonAction) onTap;
const TarBarAddButtonActionCell({
required this.action,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
'${LocaleKeys.grid_createView.tr()} ${action.title}',
color: AFThemeExtension.of(context).textColor,
),
leftIcon: svgWidget(
action.iconName,
color: Theme.of(context).iconTheme.color,
),
onTap: () => onTap(action),
).padding(horizontal: 6.0),
);
}
}
enum AddButtonAction {
grid,
calendar,
board;
String get title {
switch (this) {
case AddButtonAction.board:
return LocaleKeys.board_menuName.tr();
case AddButtonAction.calendar:
return LocaleKeys.calendar_menuName.tr();
case AddButtonAction.grid:
return LocaleKeys.grid_menuName.tr();
default:
return "";
}
}
ViewLayoutPB get layoutType {
switch (this) {
case AddButtonAction.board:
return ViewLayoutPB.Board;
case AddButtonAction.calendar:
return ViewLayoutPB.Calendar;
case AddButtonAction.grid:
return ViewLayoutPB.Grid;
default:
return ViewLayoutPB.Grid;
}
}
String get iconName {
switch (this) {
case AddButtonAction.board:
return 'editor/board';
case AddButtonAction.calendar:
return "editor/grid";
case AddButtonAction.grid:
return "editor/grid";
default:
return "";
}
}
}
@@ -0,0 +1,31 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
extension DatabaseLayoutExtension on DatabaseLayoutPB {
String layoutName() {
switch (this) {
case DatabaseLayoutPB.Board:
return LocaleKeys.board_menuName.tr();
case DatabaseLayoutPB.Calendar:
return LocaleKeys.calendar_menuName.tr();
case DatabaseLayoutPB.Grid:
return LocaleKeys.grid_menuName.tr();
default:
return "";
}
}
String iconName() {
switch (this) {
case DatabaseLayoutPB.Board:
return 'editor/board';
case DatabaseLayoutPB.Calendar:
return "editor/grid";
case DatabaseLayoutPB.Grid:
return "editor/grid";
default:
return "";
}
}
}
@@ -42,6 +42,7 @@ class GridCellBuilder {
cellControllerBuilder: cellControllerBuilder,
key: key,
style: style,
fieldType: cellContext.fieldType,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
@@ -50,6 +51,7 @@ class GridCellBuilder {
key: key,
editable: false,
style: style,
fieldType: cellContext.fieldType,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
@@ -101,11 +103,11 @@ class BlankCell extends StatelessWidget {
}
abstract class CellEditable {
GridCellFocusListener get beginFocus;
RequestFocusListener get requestFocus;
ValueNotifier<bool> get onCellFocus;
ValueNotifier<bool> get onCellEditing;
// ValueNotifier<bool> get onCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
@@ -123,11 +125,7 @@ abstract class CellAccessory extends Widget {
abstract class GridCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) {
onCellEditing.addListener(() {
onCellFocus.value = onCellEditing.value;
});
}
GridCellWidget({super.key});
@override
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
@@ -136,8 +134,8 @@ abstract class GridCellWidget extends StatefulWidget
@override
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
@override
final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
// @override
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
@override
List<GridCellAccessoryBuilder> Function(
@@ -145,7 +143,7 @@ abstract class GridCellWidget extends StatefulWidget
)? get accessoryBuilder => null;
@override
final GridCellFocusListener beginFocus = GridCellFocusListener();
final RequestFocusListener requestFocus = RequestFocusListener();
@override
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
@@ -154,7 +152,7 @@ abstract class GridCellWidget extends StatefulWidget
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void initState() {
widget.beginFocus.setListener(() => requestBeginFocus());
widget.requestFocus.setListener(requestBeginFocus);
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
Clipboard.getData("text/plain").then((data) {
@@ -170,17 +168,18 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) {
widget.beginFocus.setListener(() => requestBeginFocus());
widget.requestFocus.setListener(requestBeginFocus);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.beginFocus.removeAllListener();
widget.requestFocus.removeAllListener();
super.dispose();
}
/// Subclass can override this method to request focus.
void requestBeginFocus();
String? onCopy() => null;
@@ -188,9 +187,9 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
void onInsert(String value) {}
}
abstract class GridFocusNodeCellState<T extends GridCellWidget>
abstract class GridEditableTextCell<T extends GridCellWidget>
extends GridCellState<T> {
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
SingleListenerFocusNode get focusNode;
@override
void initState() {
@@ -224,9 +223,9 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
}
void _listenOnFocusNodeChanged() {
widget.onCellEditing.value = focusNode.hasFocus;
widget.onCellFocus.value = focusNode.hasFocus;
focusNode.setListener(() {
widget.onCellEditing.value = focusNode.hasFocus;
widget.onCellFocus.value = focusNode.hasFocus;
focusChanged();
});
}
@@ -234,7 +233,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
Future<void> focusChanged() async {}
}
class GridCellFocusListener extends ChangeNotifier {
class RequestFocusListener extends ChangeNotifier {
VoidCallback? _listener;
void setListener(VoidCallback listener) {
@@ -51,8 +51,8 @@ class CellContainer extends StatelessWidget {
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => child.beginFocus.notify(),
behavior: HitTestBehavior.opaque,
onTap: () => child.requestFocus.notify(),
child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus),
@@ -45,14 +45,14 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
});
return GridChecklistCellEditor(
cellController:
widget.cellControllerBuilder.build() as ChecklistCellController,
);
},
onClose: () => widget.onCellEditing.value = false,
onClose: () => widget.onCellFocus.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -22,11 +23,17 @@ abstract class GridCellDelegate {
class GridDateCell extends GridCellWidget {
final bool editable;
/// The [GridDateCell] is used by Field Type [FieldType.DateTime],
/// [FieldType.CreatedTime], [FieldType.LastEditedTime]. So it needs
/// to know the field type.
final FieldType fieldType;
final CellControllerBuilder cellControllerBuilder;
late final DateCellStyle? cellStyle;
GridDateCell({
GridCellStyle? style,
required this.fieldType,
required this.cellControllerBuilder,
this.editable = true,
Key? key,
@@ -65,17 +72,9 @@ class _DateCellState extends GridCellState<GridDateCell> {
value: _cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
builder: (context, state) {
Widget dateTextWidget = SizedBox.expand(
child: Align(
alignment: alignment,
child: Padding(
padding: GridSize.cellContentInsets,
child: FlowyText.medium(
state.dateStr,
overflow: TextOverflow.ellipsis,
),
),
),
Widget dateTextWidget = GridDateCellText(
dateStr: state.dateStr,
alignment: alignment,
);
// If the cell is editable, wrap it in a popover.
@@ -91,11 +90,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
return DateCellEditor(
cellController: widget.cellControllerBuilder.build()
as DateCellController,
onDismissed: () => widget.onCellEditing.value = false,
onDismissed: () => widget.onCellFocus.value = false,
);
},
onClose: () {
widget.onCellEditing.value = false;
widget.onCellFocus.value = false;
},
);
}
@@ -116,10 +115,36 @@ class _DateCellState extends GridCellState<GridDateCell> {
_popover.show();
if (widget.editable) {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
}
}
@override
String? onCopy() => _cellBloc.state.dateStr;
}
class GridDateCellText extends StatelessWidget {
final String dateStr;
final Alignment alignment;
const GridDateCellText({
required this.dateStr,
required this.alignment,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Align(
alignment: alignment,
child: Padding(
padding: GridSize.cellContentInsets,
child: FlowyText.medium(
dateStr,
overflow: TextOverflow.ellipsis,
),
),
),
);
}
}
@@ -123,7 +123,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
const TypeOptionSeparator(spacing: 12.0),
const _IncludeTimeButton(),
const TypeOptionSeparator(spacing: 12.0),
_DateTypeOptionButton(popoverMutex: popoverMutex)
DateTypeOptionButton(popoverMutex: popoverMutex)
];
return ListView.builder(
@@ -337,9 +337,10 @@ class _TimeTextFieldState extends State<_TimeTextField> {
}
}
class _DateTypeOptionButton extends StatelessWidget {
@visibleForTesting
class DateTypeOptionButton extends StatelessWidget {
final PopoverMutex popoverMutex;
const _DateTypeOptionButton({
const DateTypeOptionButton({
required this.popoverMutex,
Key? key,
}) : super(key: key);
@@ -16,13 +16,16 @@ class GridNumberCell extends GridCellWidget {
}) : super(key: key);
@override
GridFocusNodeCellState<GridNumberCell> createState() => _NumberCellState();
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
late NumberCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
@@ -62,7 +62,7 @@ class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: widget.onCellEditing,
onCellEditing: widget.onCellFocus,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
@@ -125,7 +125,7 @@ class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
return SelectOptionWrap(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onCellEditing: widget.onCellEditing,
onCellEditing: widget.onCellFocus,
popoverController: _popover,
cellControllerBuilder: widget.cellControllerBuilder,
);
@@ -157,7 +157,6 @@ class _TextField extends StatelessWidget {
options: state.options,
selectedOptionMap: optionMap,
distanceToText: _editorPanelWidth * 0.7,
maxLength: 30,
tagController: tagController,
textSeparators: const [','],
onClick: () => popoverMutex.close(),
@@ -337,7 +336,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
offset: const Offset(8, 0),
margin: EdgeInsets.zero,
asBarrier: true,
constraints: BoxConstraints.loose(const Size(200, 460)),
constraints: BoxConstraints.loose(const Size(200, 470)),
mutex: widget.popoverMutex,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
@@ -41,13 +41,16 @@ class GridTextCell extends GridCellWidget {
}
@override
GridFocusNodeCellState<GridTextCell> createState() => _GridTextCellState();
GridEditableTextCell<GridTextCell> createState() => _GridTextCellState();
}
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
@@ -108,11 +108,14 @@ class GridURLCell extends GridCellWidget {
}
}
class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
final _popoverController = PopoverController();
late final URLCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
@@ -182,7 +185,7 @@ class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
@override
void requestBeginFocus() {
widget.onCellEditing.value = true;
widget.onCellFocus.value = true;
_popoverController.show();
}
@@ -186,7 +186,7 @@ class EmojiButton extends StatelessWidget {
height: _kBannerActionHeight,
width: _kBannerActionHeight,
child: FlowyButton(
margin: const EdgeInsets.all(4),
margin: EdgeInsets.zero,
text: FlowyText.medium(
emoji,
fontSize: 30,
@@ -1,9 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -35,8 +37,9 @@ class RowDocument extends StatelessWidget {
loading: () => const Center(
child: CircularProgressIndicator.adaptive(),
),
error: (error) => FlowyErrorPage(
error: (error) => FlowyErrorPage.message(
error.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
finish: () => RowEditor(
viewPB: state.viewPB!,
@@ -94,8 +97,9 @@ class _RowEditorState extends State<RowEditor> {
),
finish: (result) {
return result.fold(
(error) => FlowyErrorPage(
(error) => FlowyErrorPage.message(
error.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
(_) {
final editorState = documentBloc.editorState;
@@ -85,8 +85,8 @@ class _PropertyCellState extends State<_PropertyCell> {
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final gesture = GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => cell.beginFocus.notify(),
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
child: cell,
@@ -97,8 +97,8 @@ class _PropertyCellState extends State<_PropertyCell> {
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
AppFlowyPopover(
controller: popover,
@@ -108,6 +108,7 @@ class _PropertyCellState extends State<_PropertyCell> {
popupBuilder: (popoverContext) => buildFieldEditor(),
child: SizedBox(
width: 150,
height: 40,
child: FieldCellButton(
field: widget.cellContext.fieldInfo.field,
onTap: () => popover.show(),
@@ -1,5 +1,4 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
@@ -9,6 +8,7 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import '../../grid/presentation/layout/sizes.dart';
import 'setting_button.dart';
class DatabaseSettingList extends StatelessWidget {
final DatabaseController databaseContoller;
@@ -1,9 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -97,7 +97,7 @@ class _DatabaseSettingListPopoverState
case DatabaseSettingAction.showLayout:
return DatabaseLayoutList(
viewId: widget.databaseController.viewId,
currentLayout: widget.databaseController.databaseLayout!,
currentLayout: widget.databaseController.databaseLayout,
);
case DatabaseSettingAction.showGroup:
return DatabaseGroupList(
@@ -132,7 +132,7 @@ class ICalendarSettingImpl extends ICalendarSetting {
@override
void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) {
_databaseController.updateCalenderLayoutSetting(layoutSettings);
_databaseController.updateLayoutSetting(layoutSettings);
}
@override
@@ -140,3 +140,63 @@ class ICalendarSettingImpl extends ICalendarSetting {
return _databaseController.databaseLayoutSetting?.calendar;
}
}
enum DatabaseSettingAction {
showProperties,
showLayout,
showGroup,
showCalendarLayout,
}
extension DatabaseSettingActionExtension on DatabaseSettingAction {
String iconName() {
switch (this) {
case DatabaseSettingAction.showProperties:
return 'grid/setting/properties';
case DatabaseSettingAction.showLayout:
return 'grid/setting/database_layout';
case DatabaseSettingAction.showGroup:
return 'grid/setting/group';
case DatabaseSettingAction.showCalendarLayout:
return 'grid/setting/calendar_layout';
}
}
String title() {
switch (this) {
case DatabaseSettingAction.showProperties:
return LocaleKeys.grid_settings_Properties.tr();
case DatabaseSettingAction.showLayout:
return LocaleKeys.grid_settings_databaseLayout.tr();
case DatabaseSettingAction.showGroup:
return LocaleKeys.grid_settings_group.tr();
case DatabaseSettingAction.showCalendarLayout:
return LocaleKeys.calendar_settings_name.tr();
}
}
}
/// Returns the list of actions that should be shown for the given database layout.
List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showGroup,
];
case DatabaseLayoutPB.Calendar:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showCalendarLayout,
];
case DatabaseLayoutPB.Grid:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
];
default:
return [];
}
}
@@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
show EditorState, LogLevel;
show EditorState, LogLevel, TransactionTime;
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/foundation.dart';
@@ -149,8 +149,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
this.editorState = editorState;
// subscribe to the document change from the editor
_subscription = editorState.transactionStream.listen((transaction) async {
await _transactionAdapter.apply(transaction, editorState);
_subscription = editorState.transactionStream.listen((event) async {
final time = event.$1;
if (time != TransactionTime.before) {
return;
}
await _transactionAdapter.apply(event.$2, editorState);
});
// output the log from the editor when debug mode
@@ -117,12 +117,13 @@ extension NodeToBlock on Node {
BlockPB toBlock({
String? parentId,
String? childrenId,
Attributes? attributes,
}) {
assert(id.isNotEmpty);
final block = BlockPB.create()
..id = id
..ty = type
..data = _dataAdapter(type, attributes);
..data = _dataAdapter(type, attributes ?? this.attributes);
if (childrenId != null && childrenId.isNotEmpty) {
block.childrenId = childrenId;
}
@@ -10,7 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
UpdateOperation,
DeleteOperation,
PathExtensions,
Node;
Node,
composeAttributes;
import 'package:collection/collection.dart';
import 'dart:async';
@@ -34,7 +35,8 @@ class TransactionAdapter {
final actions = transaction.operations
.map((op) => op.toBlockAction(editorState))
.whereNotNull()
.expand((element) => element);
.expand((element) => element)
.toList(growable: false); // avoid lazy evaluation
// Log.debug('actions => $actions');
await documentService.applyAction(
documentId: documentId,
@@ -114,7 +116,10 @@ extension on UpdateOperation {
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
assert(parentId.isNotEmpty);
final payload = BlockActionPayloadPB()
..block = node.toBlock()
..block = node.toBlock(
parentId: parentId,
attributes: composeAttributes(oldAttributes, attributes),
)
..parentId = parentId;
actions.add(
BlockActionPB()
@@ -132,7 +137,9 @@ extension on DeleteOperation {
final parentId =
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
final payload = BlockActionPayloadPB()
..block = node.toBlock()
..block = node.toBlock(
parentId: parentId,
)
..parentId = parentId;
assert(parentId.isNotEmpty);
actions.add(
@@ -50,10 +50,7 @@ class DocumentPlugin extends Plugin<int> {
required ViewPB view,
bool listenOnViewChanged = false,
Key? key,
}) : notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
) {
}) : notifier = ViewPluginNotifier(view: view) {
_pluginType = pluginType;
_documentAppearanceCubit.fetch();
}
@@ -126,7 +123,10 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
Widget? get rightBarItem {
return Row(
children: [
DocumentShareButton(view: view),
DocumentShareButton(
key: ValueKey(view.id),
view: view,
),
const SizedBox(width: 10),
BlocProvider.value(
value: documentAppearanceCubit,
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
@@ -14,11 +15,11 @@ import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
hide DocumentEvent;
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
class DocumentPage extends StatefulWidget {
@@ -65,7 +66,10 @@ class _DocumentPageState extends State<DocumentPage> {
return state.loadingState.when(
loading: () => const SizedBox.shrink(),
finish: (result) => result.fold(
(error) => FlowyErrorPage(error.toString()),
(error) => FlowyErrorPage.message(
error.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
(data) {
if (state.forceClose) {
widget.onDeleted();
Loaded 100 of 302 files, more files were not shown because too many files have changed in this diff. Show more