Compare commits

...
84 Commits
Author SHA1 Message Date
Lucas.Xu ffdf5d24a0 fix: openAI image expiration (#3660)
* feat: save the openAI image to local storage

* feat: support rendering error block

* fix: enter on Toggle list moves heading down without contents
2023-10-10 12:43:31 +08:00
Dhruv Sharma b247a49417 chore: resolve issue (#3621) 2023-10-09 20:47:24 +02:00
Lucas.Xu dace02d34d feat: generate image by Stability AI / openAI (#3642) 2023-10-09 23:14:24 +08:00
Muhammad Saad Tanveer 41d4351176 feat: 3625 Added Ukrainian Language (#3644) 2023-10-09 12:19:35 +08:00
Lucas.Xu d4bc575c03 fix: image block issues (#3637) 2023-10-07 13:45:38 +08:00
Nathan.fooo a59561aee3 chore: sign in with provider (#3592)
* chore: sign in with provider

* feat: implement oauth flow of appflowy cloud

* chore: rename env

* chore: fix deadlock

* fix: login bugs

* chore: clippyt

* chore: update client api

* chore: fmt
2023-10-07 09:58:44 +08:00
Mathias Mogensen 92523321f1 fix: notification button delayed tooltip (#3635) 2023-10-06 09:29:36 +02:00
Richard Shiue 537c9d0865 fix: wrong date selected in CDT (#3627) 2023-10-06 10:13:46 +08:00
Richard Shiue b4051c7e9f fix: url underline issue (#3618) 2023-10-05 17:35:50 +08:00
Richard Shiue 9e61133886 chore: bump version 0.3.5 (#3616) 2023-10-05 12:01:38 +08:00
Lucas.Xu 36f47f3636 feat: image block revamp (#3615) 2023-10-05 10:40:41 +08:00
Mayur Mahajan 40dcd13394 refactor: ui improvement in appearance (#3553) 2023-10-05 09:48:19 +08:00
Yijing Huang eb20c7c117 feat: emoji picker improvement (#3591) 2023-10-05 09:36:29 +08:00
Devanshu Rawat e9852a3934 fix: restore page banner ui improvement (#3604) 2023-10-05 09:34:36 +08:00
Alazar GeneneandAlazar Genene 78d8ec9a3e fix: use Image.file instead of DecoratedBox and FileImage (#3606)
Co-authored-by: Alazar Genene <alazargenene@Alazars-MacBook-Pro-2.local>
2023-10-04 23:48:31 +02:00
Juan David 4220309cf8 Updated translation part in project readme (#3609) 2023-10-04 15:50:42 +02:00
Jaskeerat Singh 812af0979e Fix: Missing WhiteSpace b/w Primary Field Text and Row Icon (#3602)
* Update link to OS Specific Development

* fix: Missing white space b/w primary field text & row's icon

* fix: Updated emoji padding
2023-10-04 11:50:26 +02:00
Narayan cca5005541 fix: remove current page link option in page reference (#3600)
* fix: remove current page link option in page reference

* fix: removed context and added view id as parameter
2023-10-04 11:49:22 +02:00
Mathias Mogensen 9306d4a4ee docs: update links in readm (#3589) 2023-10-04 11:35:37 +02:00
Ahsan AhmadandLucas.Xu 3ab1ff5e73 fix:The placeholder text for URL cells shouldn't be underlined (#3565)
* fix:The placeholder text for URL cells shouldn't be underlined

* Update frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart

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

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-10-04 11:35:02 +02:00
Aditya AroraandMathias Mogensen 05fa306782 feat: Delay tooltip presentations by 300ms (#3558)
* feat:added wait duration to ToolTip widgets

* feat:added wait Duration to more tooltips

* feat:added wait Duration to more_button tooltip

* fix: use flowytooltip

* fix:undo install_macos.sh file

* fix:undo install_macos.sh file

* fix:refactored FlowyTooltip.delayedTooltip to FlowyTooltip.delayed

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2023-10-04 11:34:44 +02:00
Mathias Mogensen d054439df4 fix: provide default options for inline actions always (#3601) 2023-10-03 18:07:46 +02:00
Jaskeerat Singh 15aaacc4b7 Update link to OS Specific Development (#3590) 2023-10-02 20:12:02 +02:00
Mathias Mogensen ef9de52cbc docs: update link to conventions (#3581) 2023-10-02 15:48:02 +02:00
Lucas.Xu 8d9d0e9358 chore: bump version 0.3.4 (#3572)
* chore: bump version 0.3.4

* fix: docker file build
2023-10-02 20:48:56 +08:00
7f44b181bd feat: integrate client-api (#3430)
* chore: update client-api rev

* chore: update collab rev id

* feat: add sign_in_request and import shared entity

* feat: added to userworkspace from af_workspace

* chore: add script to update the client-api rev id

* chore: update client-api rev

* feat: add workspaces api

* feat: added check user

* chore: config

* chore: update client_api version

* chore: ws connect

* chore: ws connect

* chore: update crate versions

* chore: rename event

* chore: update client-appi

* chore: set appflowy cloud env

* chore: add env template

* chore: update env name

* docs: update docs

* fix: check_user

* feat: impl sign_in_with_url

* feat: add file storage placeholders

* chore: update client-api

* chore: disable test

* feat: impl workspace add and remove

* chore: sign up test

* feat: select cover image on upload (#3488)

* fix: close popover after item selection in settings view (#3362)

* fix: close popover after item selection in settings view

* fix: add missing await before closing popover

* fix: find popover container by context instead of passing controllers around

* fix: add requested changes

* feat: close text direction settings popups after selection

* fix: clean up

* fix: restore theme value dropdown as StatefulWidget

* feat: openai and stabilityai integration (#3439)

* chore: create trait

* test: add tests

* chore: remove log

* chore: disable log

* chore: checklist ux flow redesign (#3418)

* chore: ux flow redesign

* chore: remove unused imports

* fix: allow creation of tasks of the same name

* chore: apply code suggestions from Mathias

* fix: add padding below field title text field (#3440)

* Fixed Issue no #3426

* Reversed the pubspec.lock mistaken update

* FIXED PADDING

* Fixed Padding issue on calender field edit popup

* chore: rename package name (#3501)

* fix: right icon size sam as left one (#3494)

* feat: enable removing user icon (#3487)

* feat: enable removing user icon

* fix: generate to true

* fix: review comments

* fix: more review comments

* fix: integration test and final changes

* fix: made cursor grab and background color when hovering on Appearance Options Buttons (#3498)

* chore: calendar UI polish (#3484)

* chore: update calendar theming

* feat: add event popup editor

* chore: new event button redesign and add card shadows

* chore: unscheduled events button

* chore: event title text field

* fix: focus node double dispose

* chore: show popover when create new event

* test: integrate some tests for integration testing purposes

* fix: some fixes and more integration tests

* chore: add more space between font item and font menu

* feat: add reset font button in toolbar

* feat: only show text direction toolbar item when RTL is enabled

* fix:  unable to change RTL of heading block

* test: add integration test for ltr/rtl mode

* chore: update inlang project settings (#3441)

* feat: using script to update the collab source. (#3508)

* chore: add script

* chore: update script

* chore: update bytes version

* chore: submit lock file

* chore: update test

* chore: update test

* chore: bump version

* chore: update

* ci: fix

* ci: fix

* chore: update commit id

* chore: update commit id

* chore: update commit id

* fix: is cloud enable

---------

Co-authored-by: Fu Zi Xiang <speed2exe@live.com.sg>
Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: Vincenzo De Petris <37916223+vincendep@users.noreply.github.com>
Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
Co-authored-by: Aryan More <61151896+aryan-more@users.noreply.github.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Nitin-Poojary <70025277+Nitin-Poojary@users.noreply.github.com>
Co-authored-by: Jannes Blobel <72493222+jannesblobel@users.noreply.github.com>
2023-10-02 17:22:22 +08:00
Aman Negi 01c3fec5aa feat: improve snackbar UI style (#3562) 2023-10-02 15:14:40 +08:00
Mathias Mogensen 94a3a59001 fix: avoid non-null promise on null value (#3569) 2023-10-02 15:13:39 +08:00
Mathias Mogensen 4a433a3176 feat: reminder (#3374) 2023-10-02 15:12:24 +08:00
Ricky Putra f7749bdccc chore: add integration tests for user language settings (#3560) 2023-10-02 13:55:57 +08:00
Lucas.Xu c864e836ee feat: integrate find and replace into AppFlowy (#3566)
* chore: update editor version

* feat: redesign find and replace ui

* chore: update language file
2023-10-02 13:54:51 +08:00
Richard Shiue 0738b5f87d feat: show hidden fields in row detail page (#3545) 2023-10-02 10:52:22 +08:00
Anurag De 6198c3907f fix: updated githook helper link (#3563) 2023-10-02 10:07:51 +08:00
Ahsan AhmadandMathias Mogensen 891d794900 fix:Spacing above New row in grid (#3564)
* fix:Spacing above New row in grid

* revert: update frontend/appflowy_flutter/ios/Podfile.lock

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-10-01 23:22:21 +02:00
Yijing Huang 890f52a00a feat: dandelion theme improvement (#3550) 2023-09-28 14:18:40 +08:00
Yijing Huang d007f98af6 fix: emoji picker menu issue (#3551) 2023-09-28 14:18:01 +08:00
Yijing Huang 15e9d65798 feat: go_router refactor and bottom navigation bar in mobile (#3459) 2023-09-28 14:17:38 +08:00
Lakhan Baheti 0626f0deb7 fix: on hover change_cover button correct mouse cursor (#3548) 2023-09-27 22:59:33 +08:00
Richard Shiue 007c649c31 fix: still show drag icon while dragging (#3547)
* fix: still show drag icon while dragging

* chore: use click cursor on windows
2023-09-27 20:02:44 +08:00
Norbert 34ea508f5b chore: update pl-PL translations (#3539) 2023-09-27 15:35:45 +08:00
fangwufeng-v f70aef68be feat: drag and drop a row in the Grid (#3376)
* feat: new style ui

* feat: drag row

* feat: hooks for auto scroll feature when dragging

* feat: shared UI status that control auto scroll when dragging row.

* feat: optimize the drag interaction UI effect

* feat: refactor drag positon

* feat: drag column header

* feat: fix review issue
2023-09-27 15:13:25 +08:00
Borris 61a31c90ee chore: update translations for id-ID (#3518) 2023-09-27 11:32:49 +08:00
Richard Shiue c7e3ed805a fix: checklist cell style for checklist cells (#3538) 2023-09-26 22:31:15 +08:00
Lucas.Xu d36a846425 chore: update CHANGELOG.md 2023-09-26 16:55:49 +08:00
Lucas.Xu a02ec98e9c fix: undo failed in nested list 2023-09-26 16:55:49 +08:00
Lucas.Xu 32c06cf1bf fix: clear selection when editing inline database view 2023-09-26 16:55:49 +08:00
Lucas.Xu cf1096b516 fix: undo failed in nested list 2023-09-26 16:55:49 +08:00
Lucas.Xu 938953d7b3 fix: unable to parse html text which contains <mark> tag (#3529) 2023-09-26 14:37:10 +08:00
Mathias Mogensen 949dab9afe fix: on hover/enter issue for view items (#3528) 2023-09-25 12:12:43 +02:00
Narayan 0996f5fd24 fix: missing the hand gesture icon in the text alignment options (#3509) 2023-09-24 21:42:11 +08:00
Lucas.Xu 6e887f1f12 chore: bump version 0.3.3 (#3517) 2023-09-24 20:28:31 +08:00
Richard Shiue ab9338eb89 fix: some issues with the end date (#3495) 2023-09-24 20:26:18 +08:00
Lakhan Baheti fd4088aed0 fix: grid theming to bottom row count number (#3510)
* fix: theming to bottom count number

* refactor: Row to RichText
2023-09-24 20:25:47 +08:00
Richard Shiue 375ff5ed6d refactor: use same bloc in grid and cards (#3511) 2023-09-24 20:24:38 +08:00
Ahsan Ahmad 847290d9a4 fix: remove max length of 30 while creating a new select option (#3513) 2023-09-24 19:14:25 +08:00
Nathan.fooo 56687581f2 feat: using script to update the collab source. (#3508)
* chore: add script

* chore: update script

* chore: update bytes version

* chore: submit lock file
2023-09-24 15:50:58 +08:00
Jannes Blobel 3be39f5e7b chore: update inlang project settings (#3441) 2023-09-23 23:41:56 +08:00
Lucas.Xu 1997231adf Merge pull request #3500 from LucasXu0/fix_032_launch_review_issues
fix launch review(v0.3.3) issues
2023-09-23 12:50:57 +08:00
Lucas.Xu 89face4f02 test: add integration test for ltr/rtl mode 2023-09-23 11:31:08 +08:00
Richard Shiue 37ddce3a29 chore: calendar UI polish (#3484)
* chore: update calendar theming

* feat: add event popup editor

* chore: new event button redesign and add card shadows

* chore: unscheduled events button

* chore: event title text field

* fix: focus node double dispose

* chore: show popover when create new event

* test: integrate some tests for integration testing purposes

* fix: some fixes and more integration tests
2023-09-23 09:47:48 +08:00
Nitin-Poojary 593df96b10 fix: made cursor grab and background color when hovering on Appearance Options Buttons (#3498) 2023-09-22 19:03:18 +02:00
Mathias Mogensen 1b966171c4 feat: enable removing user icon (#3487)
* feat: enable removing user icon

* fix: generate to true

* fix: review comments

* fix: more review comments

* fix: integration test and final changes
2023-09-22 15:20:07 +02:00
Lakhan Baheti 047f1a0b39 fix: right icon size sam as left one (#3494) 2023-09-22 12:25:27 +02:00
Lucas.Xu f2f3506b29 fix: unable to change RTL of heading block 2023-09-22 17:13:08 +08:00
Lucas.Xu 0ccced4b54 chore: rename package name (#3501) 2023-09-22 16:32:18 +08:00
Lucas.Xu f762a564b5 feat: only show text direction toolbar item when RTL is enabled 2023-09-22 15:59:21 +08:00
Lucas.Xu 57b78ee3c6 feat: add reset font button in toolbar 2023-09-22 15:49:11 +08:00
Lucas.Xu 5dfc470873 chore: add more space between font item and font menu 2023-09-22 13:53:14 +08:00
Aryan More d5f8ce5435 fix: add padding below field title text field (#3440)
* Fixed Issue no #3426

* Reversed the pubspec.lock mistaken update

* FIXED PADDING

* Fixed Padding issue on calender field edit popup
2023-09-22 11:15:18 +08:00
Richard Shiue 3c65a96b04 chore: checklist ux flow redesign (#3418)
* chore: ux flow redesign

* chore: remove unused imports

* fix: allow creation of tasks of the same name

* chore: apply code suggestions from Mathias
2023-09-22 09:33:24 +08:00
Nathan.fooo 6ba7fc0317 feat: openai and stabilityai integration (#3439)
* chore: create trait

* test: add tests

* chore: remove log

* chore: disable log
2023-09-21 12:41:52 +08:00
Vincenzo De Petris 37a5054ebd fix: close popover after item selection in settings view (#3362)
* fix: close popover after item selection in settings view

* fix: add missing await before closing popover

* fix: find popover container by context instead of passing controllers around

* fix: add requested changes

* feat: close text direction settings popups after selection

* fix: clean up

* fix: restore theme value dropdown as StatefulWidget
2023-09-21 11:23:28 +08:00
Mathias Mogensen 048434024b feat: select cover image on upload (#3488) 2023-09-21 04:02:39 +02:00
Nitin-Poojary 2c757e9b6c fix: set minimum width for grid fields to 50px (#3397)
* fix: set minimum width for grid field to 100px

* test: add grid field width test

* fix: field width should not be less then 50px

* test: grid field width should not be less then 50px

* fix: cursor-based resizing issue

* test: updated tests
2023-09-21 09:39:13 +08:00
Lucas.Xu 9c59e1487e feat: customize text font (#3467)
* feat: update UI in settings page

* feat: customzing font in document page

* fix: flutter analyze and format issues
2023-09-21 09:12:25 +08:00
Mathias Mogensen 4b9b723521 fix: add bottom border to new property column (#3486) 2023-09-21 03:10:41 +02:00
Vyshnav Ajith a389836897 Added Language Support for Urdu. Issue#3372 (#3469)
* Create ur.json

* Add files via upload

* changes

* Added Locale change to the supported Locales List

* Added Urdu in Language.dart

* Delete .vscode/launch.json

* Changed the icon for Calendar
2023-09-20 17:31:17 +02:00
Mathias Mogensen eec7723ce0 fix: tests for interacting with viewitem (#3472) 2023-09-20 23:14:04 +08:00
Lucas.Xu 2f938a331f Merge pull request #3468 from LucasXu0/fix_build_error_on_xcode15
fix: build error on Xcode 15
2023-09-20 20:32:28 +08:00
Lucas.Xu 847d7cb35a fix: build error on Xcode 15 2023-09-20 15:28:27 +08:00
Onyedika Israel Ukwueze 094f8fc307 fix: visual bug on board accessory 2023-09-19 16:02:19 +08:00
Yatendra Kumarandnathan e3f11ea9c0 feat: integrated openAI rust plugin (#3302)
* feat: create flowy-plugins

* feat: integrated openai service with interface

* feat: integrated openai api call

* fix: response in api

* fix: removed unused imports

* fix: remove parse.rs

* fix: macos version + removed print

* fix: removed debugPrint + changed deployment target

* fix: android project+removed gesture detector

* fix: removed unused imports

* chore: fix compile

* chore: rever changes

* chore: rever changes

* chore: fix clippy warnings

* chore: rename

* chore: fix compile error

* chore: remove dart ai

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-09-19 10:37:37 +08:00
Richard Shiueandnathan 124d435f09 feat: add end date to time cell data (#3369)
* feat: add end date to time cell data

* feat: implement ui for end time

* test: add date to text test

* chore: clippy warnings

* fix: unexpected time parsing

* fix: fix the date logic when toggling end date

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-09-19 09:58:15 +08:00
Mathias Mogensen b700f95c7f fix: do not rebuild hover on item selection (#3423) 2023-09-18 16:39:30 +08:00
514 changed files with 20407 additions and 11412 deletions

No files matched your search

+1 -1
View File
@@ -44,7 +44,7 @@ test "" = "$(grep '^Signed-off-by: ' "$1" |
if [ $? -ne 0 ]
then
printError "Please fix your commit message to match AppFlowy coding standards"
printError "https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/code-submission-guidelines#commit-message-guidelines"
printError "https://docs.appflowy.io/docs/documentation/software-contributions/conventions/git-conventions"
exit 1
fi
+1 -1
View File
@@ -21,7 +21,7 @@ Before you mark this PR ready for review, run through this checklist!
#### PR Checklist
- [ ] My code adheres to the [AppFlowy Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides)
- [ ] My code adheres to [AppFlowy's Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions)
- [ ] I've listed at least one issue that this PR fixes in the description above.
- [ ] I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
- [ ] All existing tests are passing.
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install keybinder-3.0 libnotify-dev
elif [ "$RUNNER_OS" == "Windows" ]; then
vcpkg integrate install
elif [ "$RUNNER_OS" == "macOS" ]; then
+1 -1
View File
@@ -85,7 +85,7 @@ jobs:
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install keybinder-3.0 libnotify-dev
elif [ "$RUNNER_OS" == "Windows" ]; then
vcpkg integrate install
elif [ "$RUNNER_OS" == "macOS" ]; then
+39 -15
View File
@@ -35,13 +35,15 @@ jobs:
release_name: v${{ github.ref }}
body_path: ${{ env.RELEASE_NOTES_PATH }}
# the package name should be with the format: AppFlowy-<version>-<os>-<arch>
build-for-windows:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
needs: create-release
env:
WINDOWS_APP_RELEASE_PATH: frontend\appflowy_flutter\product\${{ github.ref_name }}\windows
WINDOWS_ZIP_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64.zip
WINDOWS_INSTALLER_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64
WINDOWS_ZIP_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64.zip
WINDOWS_INSTALLER_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
@@ -133,8 +135,8 @@ jobs:
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
MACOS_X86_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64.zip
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64
MACOS_X86_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.zip
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64
strategy:
fail-fast: false
matrix:
@@ -235,8 +237,8 @@ jobs:
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
MACOS_AARCH64_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-universal.zip
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-universal
MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.zip
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal
strategy:
fail-fast: false
matrix:
@@ -339,9 +341,11 @@ jobs:
needs: create-release
env:
LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release
LINUX_ZIP_NAME: AppFlowy_${{ matrix.job.target }}_${{ matrix.job.os }}.tar.gz
LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_${{ matrix.job.os }}.deb
# PKG_CONFIG_SYSROOT_DIR: /
LINUX_ZIP_NAME: AppFlowy-${{ matrix.job.target }}-x86_64.tar.gz
LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb
LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
strategy:
fail-fast: false
matrix:
@@ -384,7 +388,8 @@ jobs:
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install keybinder-3.0 libnotify-dev
sudo apt-get -y install alien
source $HOME/.cargo/env
cargo install --force cargo-make
cargo install --force duckscript_cli
@@ -409,8 +414,17 @@ jobs:
- name: Build Linux package (.deb)
working-directory: frontend
run: |
sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_NAME }}
sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_DEB_NAME }}
- name: Build Linux package (.rpm)
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
run: |
ls -a
sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }}
echo '------------'
cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }}
ls -a
- name: Upload Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
@@ -423,15 +437,25 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Debian package
id: upload-release-asset-install-package
id: upload-release-asset-install-package-deb
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_NAME }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_DEB_NAME }}
asset_name: ${{ env.LINUX_PACKAGE_DEB_NAME }}
asset_content_type: application/octet-stream
asset_name: ${{ env.LINUX_PACKAGE_NAME }}
- name: Upload RPM package
id: upload-release-asset-install-package-rpm
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_RPM_NAME }}
asset_name: ${{ env.LINUX_PACKAGE_RPM_NAME }}
asset_content_type: application/octet-stream
build-for-docker:
+43
View File
@@ -1,5 +1,48 @@
# Release Notes
## Version 0.3.5 - 10/09/2023
### New Features
- Added support for browsing and inserting images from Unsplash.
- Revamp and unify the emoji picker throughout AppFlowy.
### Bug fixes
- Improve layout of the settings page.
- Improve design of the restore page banner.
- Improve UX of the reminders.
- Other UI fixes.
## Version 0.3.4 - 10/02/2023
### New Features
- Added support for creating a reminder.
- Added support for finding and replacing in the document page.
- Added support for showing the hidden fields in row detail page.
- Adjust the toolbar style in RTL mode.
### Bug fixes
- Improve snackbar UI design.
- Improve dandelion theme.
- Improve id-ID and pl-PL language translations.
## Version 0.3.3 - 09/24/2023
### New Features
- Added an end date field to the time cell in the database.
- Added Support for customizing the font family from GoogleFonts in the editor.
- Set the uploaded image to cover by default.
- Added Support for resetting the user icon on settings page
- Add Urdu language translations.
### Bug fixes
- Default colors for the blocks except for the callout were not transparent.
- Option/Alt + click to add a block above didn't work on the first line.
- Unable to paste HTML content containing `<mark>` tag.
- Unable to select the text from anywhere in the line.
- The selection in the editor didn't clear when editing the inline database.
- Added a bottom border to new property column in the database.
- Set minimum width of 50px for grid fields.
## Version 0.3.2 - 09/18/2023
### New Features
+2 -2
View File
@@ -47,7 +47,7 @@ You are in charge of your data and customizations.
## Getting Started with development
Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) for OS specific development instructions
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
## Roadmap
@@ -73,7 +73,7 @@ Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitt
[![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
To add translations, you can manually edit the JSON translation files in `/frontend/appflowy_flutter/assets/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
## Join the community to build AppFlowy together
+8
View File
@@ -285,6 +285,14 @@
"options": {
"cwd": "${workspaceFolder}/appflowy_flutter"
}
},
{
"label": "AF: Generate AppFlowyEnv",
"type": "shell",
"command": "dart run build_runner build --delete-conflicting-outputs",
"options": {
"cwd": "${workspaceFolder}/appflowy_flutter/packages/appflowy_backend"
}
}
]
}
+1 -1
View File
@@ -25,7 +25,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.3.2"
CURRENT_APP_VERSION = "0.3.5"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
+2 -2
View File
@@ -69,8 +69,8 @@ windows/flutter/dart_ffi/
**/.sandbox
**/.vscode/
*.env
*.env.*
.env
.env.*
coverage/
+14 -16
View File
@@ -13,11 +13,13 @@ This Repository contains the codebase for the frontend of the application, curre
- Linux
- macOS
- Windows
> We later expect to extend support to Android and iOS devices using Flutter.
> We are actively working on support for Android & iOS!
_Additionally, we are working on a Web version built with Tauri!_
### Am I Eligible to Contribute?
Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy). Some of the ways in which you can contribute are:
Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy). Some of the ways in which you can contribute are:
- Non-Coding Contributions
- Documentation
@@ -26,27 +28,23 @@ Yes! You are eligible to contribute, check out the ways in which you can [contri
- Improve Translations
- Coding Contributions
To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we may suggest you to learn the basics and then try to contribute, get started with Flutter [here](https://flutter.dev/docs/get-started/codelab).
To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we suggest you learn the basics, and then contribute afterwards. To get started with Flutter read [here](https://flutter.dev/docs/get-started/codelab).
### What OS Should I Use for Development?
### What OS should I use for development?
We support all OS for Development i.e Linux, macOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/environment-setup) on How to Setup `AppFlowy_Flutter` in your local system in each OS.
We support all OS for Development i.e. Linux, MacOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://docs.appflowy.io/docs/documentation/appflowy/from-source/environment-setup) on how to setup `AppFlowy_Flutter` on your local system respectively per operating system.
### Getting Started ❇
We have a detailed documentation, on how to [get started](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you.
We have detailed documentation on how to [get started](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you:
- [Code Architecture](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/frontend/codemap)
- [Making Your First PR](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/submitting-your-first-pull-request)
- [The Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides)
- [How to run/debug the application](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/launcher-and-tasks)
- [Styleguide & Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions/naming-conventions)
- [Making Your First PR](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/submitting-your-first-pull-request)
- [All AppFlowy Documentation](https://docs.appflowy.io/docs/documentation/appflowy) - Contribution guide, build and run, debugging, testing, localization, etc.
### Need Help?
- New to GitHub? Follow [these](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/setting-up-your-repositories) steps to get started
- Stuck Somewhere? Join the [Discord](https://discord.gg/9Q2xaN37tV) Group and we are there to help you!
<!--
## release check
1. [entitlements](https://flutter.dev/desktop#setting-up-entitlements)
2. [symbols stripped](https://flutter.dev/docs/development/platform-integration/c-interop) -->
- New to GitHub? Follow [these](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/setting-up-your-repositories) steps to get started
- Stuck Somewhere? Join our [Discord](https://discord.gg/9Q2xaN37tV), we're there to help you!
- Find out more about the [community initiatives](https://docs.appflowy.io/docs/appflowy/community).
+23
View File
@@ -0,0 +1,23 @@
# Initial Setup
# 1. Copy the dev.env file to .env:
# cp dev.env .env
# 2. Alternatively, you can generate the .env file using the "Generate Env File" task in VSCode.
# Configuring Cloud Type
# This configuration file is used to specify the cloud type and the necessary configurations for each cloud type. The available options are:
# Local: 0
# Supabase: 1
# AppFlowy Cloud: 2
CLOUD_TYPE=0
# Supabase Configuration
# If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations:
SUPABASE_URL=replace-with-your-supabase-url
SUPABASE_ANON_KEY=replace-with-your-supabase-key
# AppFlowy Cloud Configuration
# If you're using AppFlowy Cloud (CLOUD_TYPE=2), you need to provide the following configurations:
APPFLOWY_CLOUD_BASE_URL=replace-with-your-appflowy-cloud-url
APPFLOWY_CLOUD_WS_BASE_URL=replace-with-your-appflowy-cloud-ws-url
APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url
@@ -80,7 +80,8 @@ void main() {
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
expect(find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
expect(
find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
findsOneWidget,
);
});
@@ -79,33 +79,33 @@ void main() {
// 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 editor popup is shown
tester.assertEventEditorOpen();
tester.assertNumberOfEventsInCalendar(1);
// Dismiss the event editor popup
await tester.dismissEventEditor();
// 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 editor popup is shown
tester.assertEventEditorOpen();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(2);
// Dismiss the event editor popup
await tester.dismissEventEditor();
// Click on the event
await tester.openCalendarEvent(index: 0);
tester.assertRowDetailPageOpened();
tester.assertEventEditorOpen();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
await tester.dismissRowDetailPage();
await tester.editEventTitle('hello world');
await tester.dismissEventEditor();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
@@ -113,6 +113,10 @@ void main() {
// Click on the event
await tester.openCalendarEvent(index: 1);
tester.assertEventEditorOpen();
// Click on the open icon
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
// Duplicate the event
@@ -126,12 +130,23 @@ void main() {
// Delete an event
await tester.openCalendarEvent(index: 1);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.deleteEventFromEventEditor();
// Check that there is 1 event
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Delete event from row detail page
await tester.openCalendarEvent(index: 1);
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 0 event
tester.assertNumberOfEventsInCalendar(0, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());
});
testWidgets('rescheduling events', (tester) async {
@@ -145,7 +160,7 @@ void main() {
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
await tester.dismissEventEditor();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
@@ -157,13 +172,12 @@ void main() {
// Delete the event
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.deleteEventFromEventEditor();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(today);
await tester.dismissRowDetailPage();
await tester.dismissEventEditor();
// Make sure that the event is today
tester.assertNumberOfEventsOnSpecificDay(1, today);
@@ -182,12 +196,43 @@ void main() {
await tester.selectDay(content: newDate.day);
await tester.dismissCellEditor();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Dismiss the event editor
await tester.dismissEventEditor();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, newDate);
// Click on the unscheduled events button
await tester.openUnscheduledEventsPopup();
// Assert that nothing shows up
tester.findUnscheduledPopup(findsNothing, 0);
// Click on the event in the calendar
await tester.openCalendarEvent(index: 0, date: newDate);
// Open the date editor of the event
await tester.tapDateCellInRowDetailPage();
await tester.findDateEditor(findsOneWidget);
// Clear the date of the event
await tester.clearDate();
// Dismiss the event editor
await tester.dismissEventEditor();
tester.assertNumberOfEventsInCalendar(0);
// Click on the unscheduled events button
await tester.openUnscheduledEventsPopup();
// Assert that a popup appears and 1 unscheduled event
tester.findUnscheduledPopup(findsOneWidget, 1);
// Click on the unscheduled event
await tester.clickUnscheduledEvent();
tester.assertRowDetailPageOpened();
});
});
}
@@ -185,7 +185,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit time cell', (tester) async {
testWidgets('edit date time cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -456,9 +456,6 @@ void main() {
// assert that the checklist editor is shown
tester.assertChecklistEditorVisible(visible: true);
// assert that new task editor is shown
tester.assertNewCheckListTaskEditorVisible(visible: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
@@ -481,7 +478,6 @@ void main() {
// dismiss new task editor
await tester.dismissCellEditor();
tester.assertNewCheckListTaskEditorVisible(visible: false);
// dismiss checklist cell editor
await tester.dismissCellEditor();
@@ -489,11 +485,8 @@ void main() {
// assert that progress bar is shown in grid at 0%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
// start editing the first checklist cell again, click on new task button
// start editing the first checklist cell again
await tester.tapChecklistCellInGrid(rowIndex: 0);
tester.assertNewCheckListTaskEditorVisible(visible: false);
await tester.tapChecklistNewTaskButton();
tester.assertNewCheckListTaskEditorVisible(visible: true);
// create another task with the create button
await tester.createNewChecklistTask(name: "task 2", button: true);
@@ -540,9 +533,6 @@ void main() {
// delete the remaining task
await tester.deleteChecklistTask(index: 0);
// assert that the new task editor is shown
tester.assertNewCheckListTaskEditorVisible(visible: true);
// dismiss the cell editor
await tester.dismissCellEditor();
@@ -146,7 +146,7 @@ void main() {
await tester.openFirstRowDetailPage();
// Assert that the first field in the row details page is the select
// option tyoe
// option type
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
// Reorder first field in list
@@ -168,6 +168,54 @@ void main() {
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
});
testWidgets('hide and show hidden fields', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Assert that the show hidden fields button isn't visible
tester.assertToggleShowHiddenFieldsVisibility(false);
// Hide the first field in the field list
await tester.tapGridFieldWithNameInRowDetailPage("Type");
await tester.tapHidePropertyButtonInFieldEditor();
// Assert that the field is now hidden
tester.noFieldWithName("Type");
// Assert that the show hidden fields button appears
tester.assertToggleShowHiddenFieldsVisibility(true);
// Click on the show hidden fields button
await tester.toggleShowHiddenFields();
// Assert that the hidden field is shown again and that the show
// hidden fields button is still present
tester.findFieldWithName("Type");
tester.assertToggleShowHiddenFieldsVisibility(true);
// Click hide hidden fields
await tester.toggleShowHiddenFields();
// Assert that the hidden field has vanished
tester.noFieldWithName("Type");
// Click show hidden fields
await tester.toggleShowHiddenFields();
// delete the hidden field
await tester.tapGridFieldWithNameInRowDetailPage("Type");
await tester.tapDeletePropertyInFieldEditor();
// Assert that the that the show hidden fields button is gone
tester.assertToggleShowHiddenFieldsVisibility(false);
});
testWidgets('check document exists in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -0,0 +1,44 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// +, ... button beside the block component.
group('document with option action button', () {
testWidgets(
'click + to add a block after current selection, and click + and option key to add a block before current selection',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
var editorState = tester.editor.getCurrentEditorState();
expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isNotEmpty);
// add a new block after the current selection
await tester.editor.hoverAndClickOptionAddButton([0], false);
// await tester.pumpAndSettle();
expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isEmpty);
// cancel the selection menu
await tester.tapAt(Offset.zero);
await tester.editor.hoverAndClickOptionAddButton([0], true);
await tester.pumpAndSettle();
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
// cancel the selection menu
await tester.tapAt(Offset.zero);
await tester.tapAt(Offset.zero);
await tester.createNewPageWithName(name: 'test');
await tester.openPage(gettingStarted);
// check the status again
editorState = tester.editor.getCurrentEditorState();
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
});
});
}
@@ -5,8 +5,11 @@ import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
import 'document_create_and_delete_test.dart'
as document_create_and_delete_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
import 'document_with_database_test.dart' as document_with_database_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
@@ -29,4 +32,7 @@ void startTesting() {
document_copy_and_paste_test.main();
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_test.main();
document_option_action_test.main();
document_with_image_block_test.main();
}
@@ -0,0 +1,58 @@
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('text direction', () {
testWidgets(
'''no text direction items will be displayed in the default/LTR mode,and three text direction items will be displayed in the RTL mode.''',
(tester) async {
// combine the two tests into one to avoid the time-consuming process of initializing the app
await tester.initializeAppFlowy();
await tester.tapGoButton();
final selection = Selection.single(
path: [0],
startOffset: 0,
endOffset: 1,
);
// click the first line of the readme
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.updateSelection(selection);
await tester.pumpAndSettle();
// because this icons are defined in the appflowy_editor package, we can't fetch the icons by SVG data. [textDirectionItems]
final textDirectionIconNames = [
'toolbar/text_direction_auto',
'toolbar/text_direction_left',
'toolbar/text_direction_right',
];
// no text direction items in default/LTR mode
var button = find.byWidgetPredicate(
(widget) =>
widget is SVGIconItemWidget &&
textDirectionIconNames.contains(widget.iconName),
);
expect(button, findsNothing);
// switch to the RTL mode
await tester.switchLayoutDirectionMode(LayoutDirection.rtlLayout);
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.updateSelection(selection);
await tester.pumpAndSettle();
button = find.byWidgetPredicate(
(widget) =>
widget is SVGIconItemWidget &&
textDirectionIconNames.contains(widget.iconName),
);
expect(button, findsNWidgets(3));
});
});
}
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
@@ -31,6 +32,18 @@ void main() {
),
findsOneWidget,
);
// https://github.com/AppFlowy-IO/AppFlowy/issues/3533
// test: the selection of editor should be clear when editing the grid
await tester.editor.updateSelection(
Selection.collapsed(
Position(path: [1]),
),
);
final gridTextCell = find.byType(GridTextCell).first;
await tester.tapButton(gridTextCell);
expect(tester.editor.getCurrentEditorState().selection, isNull);
});
testWidgets('insert a referenced board', (tester) async {
@@ -0,0 +1,147 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:run_with_network_images/run_with_network_images.dart';
import '../util/mock/mock_file_picker.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
group('image block in document', () {
testWidgets('insert an image from local file', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName('Image');
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
await tester.tapButton(find.byType(ImagePlaceholder));
expect(find.byType(UploadImageMenu), findsOneWidget);
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(
paths: [imagePath],
);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
// remove the temp file
file.deleteSync();
});
testWidgets('insert an image from network', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName('Image');
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
await tester.tapButton(find.byType(ImagePlaceholder));
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
);
const url =
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
await tester.enterText(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField),
),
url,
);
await tester.tapButton(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.text(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
findRichText: true,
),
),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], url);
});
testWidgets('insert an image from unsplash', (tester) async {
await runWithNetworkImages(() async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName('Image');
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
await tester.tapButton(find.byType(ImagePlaceholder));
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
'Unsplash',
);
expect(find.byType(UnsplashImageWidget), findsOneWidget);
});
});
});
}
@@ -0,0 +1,118 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/base.dart';
import '../util/common_operations.dart';
import '../util/editor_test_operations.dart';
import '../util/keyboard.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Reminder in Document', () {
testWidgets('Add reminder for tomorrow, and include time', (tester) async {
const time = "23:59";
await tester.initializeAppFlowy();
await tester.tapGoButton();
final dateTimeSettings =
await UserSettingsBackendService().getDateTimeSettings();
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.getCurrentEditorState().insertNewLine();
await tester.pumpAndSettle();
// Trigger iline action menu and type 'remind tomorrow'
final tomorrow = await _insertReminderTomorrow(tester);
Node node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;
Map<String, dynamic> mentionAttr =
node.delta!.first.attributes![MentionBlockKeys.mention];
expect(node.type, 'paragraph');
expect(mentionAttr['type'], MentionType.reminder.name);
expect(mentionAttr['date'], tomorrow.toIso8601String());
await tester.tap(
find.text(dateTimeSettings.dateFormat.formatDate(tomorrow, false)),
);
await tester.pumpAndSettle();
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
await tester.enterText(find.byType(FlowyTextField), time);
// Leave text field to submit
await tester.tap(find.text(LocaleKeys.grid_field_includeTime.tr()));
await tester.pumpAndSettle();
node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;
mentionAttr = node.delta!.first.attributes![MentionBlockKeys.mention];
final tomorrowWithTime =
_dateWithTime(dateTimeSettings.timeFormat, tomorrow, time);
expect(node.type, 'paragraph');
expect(mentionAttr['type'], MentionType.reminder.name);
expect(mentionAttr['date'], tomorrowWithTime.toIso8601String());
});
});
}
Future<DateTime> _insertReminderTomorrow(WidgetTester tester) async {
await tester.editor.showAtMenu();
await FlowyTestKeyboard.simulateKeyDownEvent(
[
LogicalKeyboardKey.keyR,
LogicalKeyboardKey.keyE,
LogicalKeyboardKey.keyM,
LogicalKeyboardKey.keyI,
LogicalKeyboardKey.keyN,
LogicalKeyboardKey.keyD,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyT,
LogicalKeyboardKey.keyO,
LogicalKeyboardKey.keyM,
LogicalKeyboardKey.keyO,
LogicalKeyboardKey.keyR,
LogicalKeyboardKey.keyR,
LogicalKeyboardKey.keyO,
LogicalKeyboardKey.keyW,
],
tester: tester,
);
await FlowyTestKeyboard.simulateKeyDownEvent(
[LogicalKeyboardKey.enter],
tester: tester,
);
return DateTime.now().add(const Duration(days: 1)).withoutTime;
}
DateTime _dateWithTime(UserTimeFormatPB format, DateTime date, String time) {
final t = format == UserTimeFormatPB.TwelveHour
? DateFormat.jm().parse(time)
: DateFormat.Hm().parse(time);
return DateTime.parse(
'${date.year}${_padZeroLeft(date.month)}${_padZeroLeft(date.day)} ${_padZeroLeft(t.hour)}:${_padZeroLeft(t.minute)}',
);
}
String _padZeroLeft(int a) => a.toString().padLeft(2, '0');
@@ -22,6 +22,8 @@ import 'tabs_test.dart' as tabs_test;
import 'hotkeys_test.dart' as hotkeys_test;
import 'appearance_settings_test.dart' as appearance_test_runner;
import 'auth/auth_test.dart' as auth_test_runner;
import 'settings/user_icon_test.dart' as user_icon_test;
import 'settings/user_language_test.dart' as user_language_test;
/// The main task runner for all integration tests in AppFlowy.
///
@@ -67,7 +69,11 @@ void main() {
// Appearance integration test
appearance_test_runner.main();
if (isSupabaseEnabled) {
// User settings
user_icon_test.main();
user_language_test.main();
if (isCloudEnabled) {
auth_test_runner.main();
}
@@ -0,0 +1,54 @@
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Settings: user icon tests', () {
testWidgets('select icon, select default option', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userAvatarFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserAvatar),
);
// Open icon picker dialog
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
// Select first option that isn't default
await tester.tap(find.byType(IconOption).first);
await tester.pumpAndSettle();
UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isNotEmpty);
// Open icon picker dialog again
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
// Tap the default option
await tester.tap(
find.descendant(
of: find.byType(IconGallery),
matching: find.byType(UserAvatar),
),
);
await tester.pumpAndSettle();
userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isEmpty);
});
});
}
@@ -0,0 +1,69 @@
import 'dart:ui';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util/util.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Settings: user language tests', () {
testWidgets('select language, language changed', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.language);
final userLanguageFinder = find.descendant(
of: find.byType(SettingsLanguageView),
matching: find.byType(LanguageSelector),
);
// Grab current locale
LanguageSelector userLanguage =
tester.widget<LanguageSelector>(userLanguageFinder);
Locale currentLocale = userLanguage.currentLocale;
// Open language selector
await tester.tap(userLanguageFinder);
await tester.pumpAndSettle();
// Select first option that isn't default
await tester.tap(find.byType(LanguageItem).at(1));
await tester.pumpAndSettle();
// Make sure the new locale is not the same as previous one
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
expect(
userLanguage.currentLocale,
isNot(equals(currentLocale)),
reason: "new language shouldn't equal the previous selected language",
);
// Update the current locale to a new one
currentLocale = userLanguage.currentLocale;
// Tried the same flow for the second time
// Open language selector
await tester.tap(userLanguageFinder);
await tester.pumpAndSettle();
// Select second option that isn't default
await tester.tap(find.byType(LanguageItem).at(2));
await tester.pumpAndSettle();
// Make sure the new locale is not the same as previous one
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
expect(
userLanguage.currentLocale,
isNot(equals(currentLocale)),
reason: "new language shouldn't equal the previous selected language",
);
});
});
}
@@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/board/presentation/board_page.dar
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
@@ -25,6 +26,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
@@ -45,7 +47,6 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
@@ -53,7 +54,8 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@@ -330,7 +332,17 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> toggleIncludeTime() async {
final findDateEditor = find.byType(DateCellEditor);
final findDateEditor = find.byType(IncludeTimeButton);
final findToggle = find.byType(Toggle);
final finder = find.descendant(
of: findDateEditor,
matching: findToggle,
);
await tapButton(finder);
}
Future<void> toggleDateRange() async {
final findDateEditor = find.byType(EndTimeButton);
final findToggle = find.byType(Toggle);
final finder = find.descendant(
of: findDateEditor,
@@ -483,15 +495,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
void assertNewCheckListTaskEditorVisible({required bool visible}) {
final editor = find.byType(NewTaskItem);
if (visible) {
expect(editor, findsOneWidget);
} else {
expect(editor, findsNothing);
}
}
Future<void> createNewChecklistTask({
required String name,
enter = false,
@@ -504,10 +507,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
);
await enterText(textField, name);
await pumpAndSettle(const Duration(milliseconds: 300));
await pumpAndSettle();
if (enter) {
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle(const Duration(milliseconds: 300));
await pumpAndSettle();
} else {
await tapButton(
find.descendant(
@@ -544,11 +547,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await enterText(textField, name);
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> tapChecklistNewTaskButton() async {
await tapButton(find.byType(ChecklistNewTaskButton));
await pumpAndSettle();
}
Future<void> checkChecklistTask({required int index}) async {
@@ -675,6 +674,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
void assertToggleShowHiddenFieldsVisibility(bool shown) {
final button = find.byType(ToggleHiddenFieldsVisibilityButton);
if (shown) {
expect(button, findsOneWidget);
} else {
expect(button, findsNothing);
}
}
Future<void> toggleShowHiddenFields() async {
final button = find.byType(ToggleHiddenFieldsVisibilityButton);
await tapButton(button);
}
Future<void> tapDeletePropertyInFieldEditor() async {
final deleteButton = find.byType(DeleteFieldButton);
await tapButton(deleteButton);
final confirmButton = find.descendant(
of: find.byType(NavigatorAlertDialog),
matching: find.byType(PrimaryTextButton),
);
await tapButton(confirmButton);
}
Future<void> scrollGridByOffset(Offset offset) async {
await drag(find.byType(GridPage), offset);
await pumpAndSettle();
@@ -748,7 +772,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> tapHidePropertyButtonInFieldEditor() async {
final button = find.byType(HideFieldButton);
final button = find.byType(FieldVisibilityToggleButton);
await tapButton(button);
}
@@ -901,9 +925,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> assertRowCountInGridPage(int num) async {
final text = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == rowCountString(num),
);
final text = find.text('${rowCountString()} $num', findRichText: true);
expect(text, findsOneWidget);
}
@@ -1294,12 +1316,72 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(cards.at(index));
}
void assertEventEditorOpen() {
expect(find.byType(CalendarEventEditor), findsOneWidget);
}
Future<void> dismissEventEditor() async {
await simulateKeyEvent(LogicalKeyboardKey.escape);
}
Future<void> editEventTitle(String title) async {
final textField = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byType(FlowyTextField),
);
await enterText(textField, title);
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> openEventToRowDetailPage() async {
final button = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.full_view_s,
),
);
await tapButton(button);
}
Future<void> deleteEventFromEventEditor() async {
final button = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
),
);
await tapButton(button);
}
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
final findEventCard = find.byType(EventCard);
await drag(findEventCard.first, const Offset(0, 300));
await pumpAndSettle();
}
Future<void> openUnscheduledEventsPopup() async {
final button = find.byType(UnscheduledEventsButton);
await tapButton(button);
}
void findUnscheduledPopup(Matcher matcher, int numUnscheduledEvents) {
expect(find.byType(UnscheduleEventsList), matcher);
if (matcher != findsNothing) {
expect(
find.byType(UnscheduledEventCell),
findsNWidgets(numUnscheduledEvents),
);
}
}
Future<void> clickUnscheduledEvent() async {
final unscheduledEvent = find.byType(UnscheduledEventCell);
await tapButton(unscheduledEvent);
}
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
final findAddButton = find.byType(AddDatabaseViewButton);
await tapButton(findAddButton);
@@ -1,11 +1,13 @@
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -159,7 +161,7 @@ class EditorOperations {
/// Must call [showAtMenu] first.
Future<void> tapAtMenuItemWithName(String name) async {
final atMenuItem = find.descendant(
of: find.byType(SelectionMenuWidget),
of: find.byType(InlineActionsHandler),
matching: find.text(name, findRichText: true),
);
await tester.tapButton(atMenuItem);
@@ -174,4 +176,34 @@ class EditorOperations {
);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
}
/// hover and click on the + button beside the block component.
Future<void> hoverAndClickOptionAddButton(
Path path,
bool withModifiedKey, // alt on windows or linux, option on macos
) async {
final optionAddButton = find.byWidgetPredicate(
(widget) =>
widget is BlockComponentActionWrapper &&
widget.node.path.equals(path),
);
await tester.hoverOnWidget(
optionAddButton,
onHover: () async {
if (withModifiedKey) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
}
await tester.tapButton(
find.byWidgetPredicate(
(widget) =>
widget is BlockAddButton &&
widget.blockComponentContext.node.path.equals(path),
),
);
if (withModifiedKey) {
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
}
},
);
}
}
@@ -12,8 +12,11 @@ class MyMockClient extends Mock implements http.Client {
final requestType = request.method;
final requestUri = request.url;
if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{'content-type': 'text/event-stream'};
if (requestType == 'POST' &&
requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{
'content-type': 'text/event-stream'
};
final responseBody = Stream.fromIterable([
utf8.encode(
'{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
@@ -51,7 +54,9 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
var previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream.transform(const Utf8Decoder()).transform(const LineSplitter())) {
await for (final chunk in response.stream
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
await onStart();
final data = chunk.trim().split('data: ');
if (data[0] != '[DONE]') {
@@ -1,9 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
@@ -72,4 +74,35 @@ extension AppFlowySettings on WidgetTester {
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
}
// go to settings page and switch the layout direction
Future<void> switchLayoutDirectionMode(
LayoutDirection layoutDirection,
) async {
await openSettings();
await openSettingsPage(SettingsPage.appearance);
final button = find.byKey(const ValueKey('layout_direction_option_button'));
expect(button, findsOneWidget);
await tapButton(button);
switch (layoutDirection) {
case LayoutDirection.ltrLayout:
final ltrButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_ltr.tr(),
);
await tapButton(ltrButton);
break;
case LayoutDirection.rtlLayout:
final rtlButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_rtl.tr(),
);
await tapButton(rtlButton);
break;
}
// tap anywhere to close the settings page
await tapAt(Offset.zero);
await pumpAndSettle();
}
}
+16
View File
@@ -38,4 +38,20 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
installer.aggregate_targets.each do |target|
target.xcconfigs.each do |variant, xcconfig|
xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
end
end
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference
xcconfig_path = config.base_configuration_reference.real_path
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
end
end
end
end
@@ -0,0 +1,19 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-date/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
class DateService {
static Future<Either<FlowyError, DateTime>> queryDate(String search) async {
final query = DateQueryPB.create()..query = search;
final result = (await DateEventQueryDate(query).send()).swap();
return result.fold((l) => left(l), (r) {
final date = DateTime.tryParse(r.date);
if (date != null) {
return right(date);
}
return left(FlowyError(msg: 'Could not parse Date (NLP) from String'));
});
}
}
+82 -2
View File
@@ -1,5 +1,6 @@
// lib/env/env.dart
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:envied/envied.dart';
part 'env.g.dart';
@@ -17,6 +18,36 @@ part 'env.g.dart';
///
@Envied(path: '.env')
abstract class Env {
@EnviedField(
obfuscate: true,
varName: 'CLOUD_TYPE',
defaultValue: '0',
)
static final int cloudType = _Env.cloudType;
/// AppFlowy Cloud Configuration
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_BASE_URL',
defaultValue: '',
)
static final String afCloudBaseUrl = _Env.afCloudBaseUrl;
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
defaultValue: '',
)
static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
defaultValue: '',
)
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
// Supabase Configuration:
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_URL',
@@ -31,11 +62,60 @@ abstract class Env {
static final String supabaseAnonKey = _Env.supabaseAnonKey;
}
bool get isSupabaseEnabled {
bool get isCloudEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return Env.supabaseUrl.isNotEmpty && Env.supabaseAnonKey.isNotEmpty;
return currentCloudType().isEnabled;
} else {
return false;
}
}
bool get isSupabaseEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType() == CloudType.supabase;
} else {
return false;
}
}
bool get isAppFlowyCloudEnabled {
// Only enable appflowy cloud in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType() == CloudType.appflowyCloud;
} else {
return false;
}
}
enum CloudType {
unknown,
supabase,
appflowyCloud;
bool get isEnabled => this != CloudType.unknown;
}
CloudType currentCloudType() {
final value = Env.cloudType;
if (value == 1) {
if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
Log.error("Supabase is not configured");
return CloudType.unknown;
} else {
return CloudType.supabase;
}
}
if (value == 2) {
if (Env.afCloudBaseUrl.isEmpty || Env.afCloudWSBaseUrl.isEmpty) {
Log.error("AppFlowy cloud is not configured");
return CloudType.unknown;
} else {
return CloudType.appflowyCloud;
}
}
return CloudType.unknown;
}
@@ -23,7 +23,20 @@ ThemeData getMobileThemeData() {
primaryColor: mobileColorTheme.primary, //primary 100
primaryColorLight: const Color(0xFF57B5F8), //primary 80
dividerColor: mobileColorTheme.outline, //caption
scaffoldBackgroundColor: Colors.white,
scaffoldBackgroundColor: mobileColorTheme.background,
appBarTheme: AppBarTheme(
foregroundColor: mobileColorTheme.onBackground,
backgroundColor: mobileColorTheme.background,
elevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
fontFamily: 'Poppins',
color: mobileColorTheme.onBackground,
fontSize: 18,
fontWeight: FontWeight.w600,
letterSpacing: 0.05,
),
),
// button
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
@@ -72,7 +85,6 @@ ThemeData getMobileThemeData() {
displayLarge: TextStyle(
color: Color(0xFF57B5F8),
fontSize: 32,
fontFamily: 'Poppins',
fontWeight: FontWeight.w700,
height: 1.20,
letterSpacing: 0.16,
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// TODO(yijing): delete this after implementing the real screen inside bottom navigation bar.
/// For demonstration purposes
class DetailsPlaceholderScreen extends StatefulWidget {
/// Constructs a [DetailsScreen].
const DetailsPlaceholderScreen({
required this.label,
this.param,
this.extra,
this.withScaffold = true,
super.key,
});
/// The label to display in the center of the screen.
final String label;
/// Optional param
final String? param;
/// Optional extra object
final Object? extra;
/// Wrap in scaffold
final bool withScaffold;
@override
State<StatefulWidget> createState() => DetailsPlaceholderScreenState();
}
/// The state for DetailsScreen
class DetailsPlaceholderScreenState extends State<DetailsPlaceholderScreen> {
int _counter = 0;
@override
Widget build(BuildContext context) {
if (widget.withScaffold) {
return Scaffold(
appBar: AppBar(
title: Text('Details Screen - ${widget.label}'),
),
body: _build(context),
);
} else {
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: _build(context),
);
}
}
Widget _build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Details for ${widget.label} - Counter: $_counter',
style: Theme.of(context).textTheme.titleLarge,
),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: const Text('Increment counter'),
),
const Padding(padding: EdgeInsets.all(8)),
if (widget.param != null)
Text(
'Parameter: ${widget.param!}',
style: Theme.of(context).textTheme.titleMedium,
),
const Padding(padding: EdgeInsets.all(8)),
if (widget.extra != null)
Text(
'Extra: ${widget.extra!}',
style: Theme.of(context).textTheme.titleMedium,
),
if (!widget.withScaffold) ...<Widget>[
const Padding(padding: EdgeInsets.all(16)),
TextButton(
onPressed: () {
GoRouter.of(context).pop();
},
child: const Text(
'< Back',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
]
],
),
);
}
}
@@ -0,0 +1 @@
export 'mobile_home_page.dart';
@@ -0,0 +1,80 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/material.dart';
// TODO(yijing): This is just a placeholder for now.
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key});
static const routeName = "/MobileHomeScreen";
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([
FolderEventGetCurrentWorkspace().send(),
getIt<AuthService>().getUser(),
]),
builder: (context, snapshots) {
if (!snapshots.hasData) {
return const Center(child: CircularProgressIndicator.adaptive());
}
final workspaceSetting = snapshots.data?[0].fold(
(workspaceSettingPB) {
return workspaceSettingPB as WorkspaceSettingPB?;
},
(error) => null,
);
final userProfile =
snapshots.data?[1].fold((error) => null, (userProfilePB) {
return userProfilePB as UserProfilePB?;
});
// TODO(yijing): implement home page later
return Scaffold(
key: ValueKey(userProfile?.id),
// TODO(yijing):Need to change to workspace when it is ready
appBar: AppBar(
title: Text(
userProfile?.email.toString() ?? 'No email found',
),
actions: [
IconButton(
onPressed: () {
// TODO(yijing): Navigate to setting page
},
icon: const FlowySvg(
FlowySvgs.m_setting_m,
),
)
],
),
body: Center(
child: Column(
children: [
const Text(
'User',
),
Text(
userProfile.toString(),
),
Text('Workspace name: ${workspaceSetting?.workspace.name}'),
ElevatedButton(
onPressed: () async {
await getIt<AuthService>().signOut();
runAppFlowy();
},
child: const Text('Logout'),
)
],
),
),
);
},
);
}
}
@@ -0,0 +1,94 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class MobileBottomNavigationBar extends StatelessWidget {
/// Constructs an [MobileBottomNavigationBar].
const MobileBottomNavigationBar({
required this.navigationShell,
super.key,
});
/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
// Here, the items of BottomNavigationBar are hard coded. In a real
// world scenario, the items would most likely be generated from the
// branches of the shell route, which can be fetched using
// `navigationShell.route.branches`.
type: BottomNavigationBarType.fixed,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
// There is no text shown on the bottom navigation bar, but Exception will be thrown if label is null here.
label: 'home',
icon: const FlowySvg(FlowySvgs.m_home_unselected_lg),
activeIcon: FlowySvg(
FlowySvgs.m_home_selected_lg,
color: style.colorScheme.primary,
),
),
const BottomNavigationBarItem(
label: 'favorite',
icon: FlowySvg(FlowySvgs.m_favorite_unselected_lg),
activeIcon: FlowySvg(
FlowySvgs.m_favorite_selected_lg,
blendMode: null,
),
),
const BottomNavigationBarItem(
label: 'add',
icon: FlowySvg(
FlowySvgs.m_add_circle_xl,
blendMode: null,
),
),
BottomNavigationBarItem(
label: 'search',
icon: const FlowySvg(FlowySvgs.m_search_lg),
activeIcon: FlowySvg(
FlowySvgs.m_search_lg,
color: style.colorScheme.primary,
),
),
BottomNavigationBarItem(
label: 'notification',
icon: const FlowySvg(FlowySvgs.m_notification_unselected_lg),
activeIcon: FlowySvg(
FlowySvgs.m_notification_selected_lg,
color: style.colorScheme.primary,
),
),
],
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
),
);
}
/// Navigate to the current location of the branch at the provided index when
/// tapping an item in the BottomNavigationBar.
void _onTap(BuildContext context, int bottomBarIndex) {
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
// Navigator for the branch is restored.
navigationShell.goBranch(
bottomBarIndex,
// A common pattern when using bottom navigation bars is to support
// navigating to the initial location when tapping the item that is
// already active. This example demonstrates how to support this behavior,
// using the initialLocation parameter of goBranch.
initialLocation: bottomBarIndex == navigationShell.currentIndex,
);
}
}
@@ -1,47 +0,0 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/material.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({
super.key,
required this.userProfile,
required this.workspaceSetting,
});
static const routeName = "/MobileHomeScreen";
final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("MobileHomeScreen"),
),
// TODO(yijing): implement home page later
body: Center(
child: Column(
children: [
const Text(
'User',
),
Text(
userProfile.toString(),
),
Text('Workspace name: ${workspaceSetting.workspace.name}'),
ElevatedButton(
onPressed: () async {
await getIt<AuthService>().signOut();
runAppFlowy();
},
child: const Text('Logout'),
)
],
),
),
);
}
}
@@ -0,0 +1,4 @@
export 'home/home.dart';
export 'root_placeholder_page.dart';
export 'details_placeholder_page.dart';
export 'mobile_bottom_navigation_bar.dart';
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Widget for the root/initial pages in the bottom navigation bar.
class RootPlaceholderScreen extends StatelessWidget {
/// Creates a RootScreen
const RootPlaceholderScreen({
required this.label,
required this.detailsPath,
this.secondDetailsPath,
super.key,
});
/// The label
final String label;
/// The path to the detail page
final String detailsPath;
/// The path to another detail page
final String? secondDetailsPath;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Root of section $label'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('$label Page', style: Theme.of(context).textTheme.titleLarge),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
context.go(detailsPath, extra: '$label-XYZ');
},
child: const Text('View details'),
),
const Padding(padding: EdgeInsets.all(4)),
if (secondDetailsPath != null)
TextButton(
onPressed: () {
context.go(secondDetailsPath!);
},
child: const Text('View more details'),
),
],
),
),
);
}
}
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
@@ -73,4 +74,13 @@ class DatabaseCellContext with _$DatabaseCellContext {
/// Only the primary field can have an emoji.
String? get emoji => fieldInfo.field.isPrimary ? rowMeta.icon : null;
/// Determines whether a database cell context should be visible.
/// It will be visible when the field is not hidden or when hidden fields
/// should be shown.
bool isVisible({bool showHiddenFields = false}) {
return fieldInfo.visibility != null &&
(showHiddenFields ||
fieldInfo.visibility != FieldVisibility.AlwaysHidden);
}
}
@@ -20,11 +20,15 @@ final class DateCellBackendService {
Future<Either<Unit, FlowyError>> update({
DateTime? date,
String? time,
DateTime? endDate,
String? endTime,
required includeTime,
required isRange,
}) {
final payload = DateChangesetPB.create()
..cellId = cellId
..includeTime = includeTime;
..includeTime = includeTime
..isRange = isRange;
if (date != null) {
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
@@ -33,6 +37,13 @@ final class DateCellBackendService {
if (time != null) {
payload.time = time;
}
if (endDate != null) {
final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000;
payload.endDate = Int64(dateTimestamp);
}
if (endTime != null) {
payload.endTime = endTime;
}
return DatabaseEventUpdateDateCell(payload).send();
}
@@ -15,16 +15,16 @@ class FieldActionSheetBloc
final FieldSettingsBackendService fieldSettingsService;
FieldActionSheetBloc({required FieldContext fieldCellContext})
: fieldId = fieldCellContext.field.id,
: fieldId = fieldCellContext.fieldInfo.id,
fieldService = FieldBackendService(
viewId: fieldCellContext.viewId,
fieldId: fieldCellContext.field.id,
fieldId: fieldCellContext.fieldInfo.id,
),
fieldSettingsService =
FieldSettingsBackendService(viewId: fieldCellContext.viewId),
super(
FieldActionSheetState.initial(
TypeOptionPB.create()..field_2 = fieldCellContext.field,
TypeOptionPB.create()..field_2 = fieldCellContext.fieldInfo.field,
),
) {
on<FieldActionSheetEvent>(
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -13,13 +15,14 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
final FieldBackendService _fieldBackendSvc;
FieldCellBloc({
required FieldContext cellContext,
}) : _fieldListener = SingleFieldListener(fieldId: cellContext.field.id),
required FieldContext fieldContext,
}) : _fieldListener =
SingleFieldListener(fieldId: fieldContext.fieldInfo.id),
_fieldBackendSvc = FieldBackendService(
viewId: cellContext.viewId,
fieldId: cellContext.field.id,
viewId: fieldContext.viewId,
fieldId: fieldContext.fieldInfo.id,
),
super(FieldCellState.initial(cellContext)) {
super(FieldCellState.initial(fieldContext)) {
on<FieldCellEvent>(
(event, emit) async {
event.when(
@@ -27,10 +30,13 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
_startListening();
},
didReceiveFieldUpdate: (field) {
emit(state.copyWith(field: cellContext.field));
emit(state.copyWith(field: fieldContext.fieldInfo.field));
},
onResizeStart: () {
emit(state.copyWith(resizeStart: state.width));
},
startUpdateWidth: (offset) {
final width = state.width + offset;
final width = max(offset + state.resizeStart, 50).toDouble();
emit(state.copyWith(width: width));
},
endUpdateWidth: () {
@@ -66,6 +72,7 @@ class FieldCellEvent with _$FieldCellEvent {
const factory FieldCellEvent.initial() = _InitialCell;
const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
const factory FieldCellEvent.onResizeStart() = _OnResizeStart;
const factory FieldCellEvent.startUpdateWidth(double offset) =
_StartUpdateWidth;
const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
@@ -77,11 +84,13 @@ class FieldCellState with _$FieldCellState {
required String viewId,
required FieldPB field,
required double width,
required double resizeStart,
}) = _FieldCellState;
factory FieldCellState.initial(FieldContext cellContext) => FieldCellState(
viewId: cellContext.viewId,
field: cellContext.field,
width: cellContext.field.width.toDouble(),
field: cellContext.fieldInfo.field,
width: cellContext.fieldInfo.field.width.toDouble(),
resizeStart: 0,
);
}
@@ -367,28 +367,13 @@ class FieldController {
/// Listen for field changes in the backend.
void _listenOnFieldChanges() {
void deleteFields(List<FieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
return;
}
final List<FieldInfo> newFields = fieldInfos;
final Map<String, FieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier.fieldInfos = newFields;
}
Future<FieldInfo> attachFieldSettings(FieldInfo fieldInfo) async {
return _fieldSettingsBackendSvc
.getFieldSettings(fieldInfo.id)
.then((result) {
final fieldSettings = result.fold(
(fieldSettings) => fieldSettings,
(err) {
return null;
},
(err) => null,
);
if (fieldSettings == null) {
return fieldInfo;
@@ -400,9 +385,25 @@ class FieldController {
});
}
Future<void> insertFields(List<IndexFieldPB> insertedFields) async {
List<FieldInfo> deleteFields(List<FieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
return fieldInfos;
}
final List<FieldInfo> newFields = fieldInfos;
final Map<String, FieldIdPB> deletedFieldMap = {
for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
return newFields;
}
Future<List<FieldInfo>> insertFields(
List<IndexFieldPB> insertedFields,
List<FieldInfo> fieldInfos,
) async {
if (insertedFields.isEmpty) {
return;
return fieldInfos;
}
final List<FieldInfo> newFieldInfos = fieldInfos;
for (final indexField in insertedFields) {
@@ -414,32 +415,32 @@ class FieldController {
newFieldInfos.add(fieldInfo);
}
}
_fieldNotifier.fieldInfos = newFieldInfos;
return newFieldInfos;
}
Future<List<FieldInfo>> updateFields(List<FieldPB> updatedFieldPBs) async {
Future<(List<FieldInfo>, List<FieldInfo>)> updateFields(
List<FieldPB> updatedFieldPBs,
List<FieldInfo> fieldInfos,
) async {
if (updatedFieldPBs.isEmpty) {
return [];
return (<FieldInfo>[], fieldInfos);
}
final List<FieldInfo> newFields = fieldInfos;
final List<FieldInfo> newFieldInfo = fieldInfos;
final List<FieldInfo> updatedFields = [];
for (final updatedFieldPB in updatedFieldPBs) {
final index =
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
newFieldInfo.indexWhere((field) => field.id == updatedFieldPB.id);
if (index != -1) {
newFields.removeAt(index);
newFieldInfo.removeAt(index);
final initial = FieldInfo.initial(updatedFieldPB);
final fieldInfo = await attachFieldSettings(initial);
newFields.insert(index, fieldInfo);
newFieldInfo.insert(index, fieldInfo);
updatedFields.add(fieldInfo);
}
}
if (updatedFields.isNotEmpty) {
_fieldNotifier.fieldInfos = newFields;
}
return updatedFields;
return (updatedFields, newFieldInfo);
}
// Listen on field's changes
@@ -450,10 +451,14 @@ class FieldController {
if (_isDisposed) {
return;
}
deleteFields(changeset.deletedFields);
insertFields(changeset.insertedFields);
List<FieldInfo> updatedFields;
List<FieldInfo> fieldInfos = deleteFields(changeset.deletedFields);
fieldInfos =
await insertFields(changeset.insertedFields, fieldInfos);
(updatedFields, fieldInfos) =
await updateFields(changeset.updatedFields, fieldInfos);
final updatedFields = await updateFields(changeset.updatedFields);
_fieldNotifier.fieldInfos = fieldInfos;
for (final listener in _updatedFieldCallbacks.values) {
listener(updatedFields);
}
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -108,6 +109,6 @@ class FieldBackendService {
class FieldContext with _$FieldContext {
const factory FieldContext({
required String viewId,
required FieldPB field,
required FieldInfo fieldInfo,
}) = _FieldCellContext;
}
@@ -20,7 +20,14 @@ class FieldSettingsBackendService {
return DatabaseEventGetFieldSettings(payload).send().then((result) {
return result.fold(
(fieldSettings) => left(fieldSettings.items.first),
(repeatedFieldSettings) {
final fieldSetting = repeatedFieldSettings.items.first;
if (!fieldSetting.hasVisibility()) {
fieldSetting.visibility = FieldVisibility.AlwaysShown;
}
return left(fieldSetting);
},
(r) => right(r),
);
});
@@ -31,7 +38,18 @@ class FieldSettingsBackendService {
return DatabaseEventGetAllFieldSettings(payload).send().then((result) {
return result.fold(
(fieldSettings) => left(fieldSettings.items),
(repeatedFieldSettings) {
final fieldSettings = <FieldSettingsPB>[];
for (final fieldSetting in repeatedFieldSettings.items) {
if (!fieldSetting.hasVisibility()) {
fieldSetting.visibility = FieldVisibility.AlwaysShown;
}
fieldSettings.add(fieldSetting);
}
return left(fieldSettings);
},
(r) => right(r),
);
});
@@ -246,14 +246,11 @@ class RowCache {
// ignore: prefer_collection_literals
final cellContextMap = CellContextByFieldId();
for (final fieldInfo in _fieldDelegate.fieldInfos) {
if (fieldInfo.visibility != null &&
fieldInfo.visibility != FieldVisibility.AlwaysHidden) {
cellContextMap[fieldInfo.id] = DatabaseCellContext(
rowMeta: rowMeta,
viewId: viewId,
fieldInfo: fieldInfo,
);
}
cellContextMap[fieldInfo.id] = DatabaseCellContext(
rowMeta: rowMeta,
viewId: viewId,
fieldInfo: fieldInfo,
);
}
return cellContextMap;
}
@@ -382,8 +382,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
case FieldType.Checkbox:
final group = customData.asCheckboxGroup()!;
if (group.isCheck) {
widget =
const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
widget = const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
);
} else {
widget = const FlowySvg(FlowySvgs.uncheck_s);
}
@@ -63,6 +63,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
createEvent: (DateTime date) async {
await _createEvent(date);
},
newEventPopupDisplayed: () {
emit(state.copyWith(editingEvent: null));
},
moveEvent: (CalendarDayEvent event, DateTime date) async {
await _moveEvent(event, date);
},
@@ -378,6 +381,10 @@ class CalendarEvent with _$CalendarEvent {
CalendarEventData<CalendarDayEvent> event,
) = _DidReceiveNewEvent;
// Called after creating a new event
const factory CalendarEvent.newEventPopupDisplayed() =
_NewEventPopupDisplayed;
// Called when receive a new event
const factory CalendarEvent.didReceiveEvent(
CalendarEventData<CalendarDayEvent> event,
@@ -0,0 +1,87 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'calendar_event_editor_bloc.freezed.dart';
class CalendarEventEditorBloc
extends Bloc<CalendarEventEditorEvent, CalendarEventEditorState> {
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
final RowBackendService _rowService;
CalendarEventEditorBloc({
required this.rowController,
required this.layoutSettings,
}) : _rowService = RowBackendService(viewId: rowController.viewId),
super(CalendarEventEditorState.initial()) {
on<CalendarEventEditorEvent>((event, emit) async {
await event.when(
initial: () {
_startListening();
final cells = rowController
.loadData()
.values
.where((cellContext) => cellContext.isVisible())
.toList();
if (!isClosed) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(cells),
);
}
},
didReceiveCellDatas: (cells) {
emit(state.copyWith(cells: cells));
},
delete: () async {
final result = await _rowService.deleteRow(rowController.rowId);
result.fold((l) => null, (err) => Log.error(err));
},
);
});
}
void _startListening() {
rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
final cellData = cells.values
.where((cellContext) => cellContext.isVisible())
.toList();
add(
CalendarEventEditorEvent.didReceiveCellDatas(cellData),
);
}
},
);
}
@override
Future<void> close() async {
rowController.dispose();
return super.close();
}
}
@freezed
class CalendarEventEditorEvent with _$CalendarEventEditorEvent {
const factory CalendarEventEditorEvent.initial() = _Initial;
const factory CalendarEventEditorEvent.didReceiveCellDatas(
List<DatabaseCellContext> cells,
) = _DidReceiveCellDatas;
const factory CalendarEventEditorEvent.delete() = _Delete;
}
@freezed
class CalendarEventEditorState with _$CalendarEventEditorState {
const factory CalendarEventEditorState({
required List<DatabaseCellContext> cells,
}) = _CalendarEventEditorState;
factory CalendarEventEditorState.initial() =>
CalendarEventEditorState(cells: List.empty());
}
@@ -58,9 +58,12 @@ class UnscheduleEventsBloc
);
},
didReceiveEvent: (CalendarEventPB event) {
final events = [...state.allEvents, event];
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
allEvents: events,
unscheduleEvents:
events.where((element) => !element.isScheduled).toList(),
),
);
},
@@ -1,13 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../application/calendar_bloc.dart';
@@ -18,7 +19,7 @@ class CalendarDayCard extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final RowCache _rowCache;
final RowCache rowCache;
final List<CalendarDayEvent> events;
final void Function(DateTime) onCreateEvent;
@@ -28,18 +29,21 @@ class CalendarDayCard extends StatelessWidget {
required this.isInMonth,
required this.date,
required this.onCreateEvent,
required RowCache rowCache,
required this.rowCache,
required this.events,
Key? key,
}) : _rowCache = rowCache,
super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
Color backgroundColor = Theme.of(context).colorScheme.surface;
if (!isInMonth) {
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
Color backgroundColor = Colors.transparent;
if (date.isWeekend) {
backgroundColor = AFThemeExtension.of(context).calendarWeekendBGColor;
}
final hoverBackgroundColor =
Theme.of(context).brightness == Brightness.light
? Theme.of(context).colorScheme.secondaryContainer
: Colors.transparent;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
@@ -57,14 +61,14 @@ class CalendarDayCard extends StatelessWidget {
),
// Add a separator between the header and the content.
VSpace(GridSize.typeOptionSeparatorHeight),
const VSpace(6.0),
// List of cards or empty space
if (events.isNotEmpty)
_EventList(
events: events,
viewId: viewId,
rowCache: _rowCache,
rowCache: rowCache,
constraints: constraints,
),
],
@@ -79,34 +83,29 @@ class CalendarDayCard extends StatelessWidget {
DragTarget<CalendarDayEvent>(
builder: (context, candidate, __) {
return Stack(
fit: StackFit.expand,
children: [
if (candidate.isNotEmpty)
Container(
color: Theme.of(context)
.colorScheme
.secondaryContainer,
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
Container(
width: double.infinity,
height: double.infinity,
color:
candidate.isEmpty ? null : hoverBackgroundColor,
padding: const EdgeInsets.only(top: 5.0),
child: child,
)
),
if (candidate.isEmpty)
NewEventButton(onCreate: () => onCreateEvent(date)),
],
);
},
onWillAccept: (CalendarDayEvent? event) {
if (event == null) {
return false;
}
return !isSameDay(event.date, date);
},
onAccept: (CalendarDayEvent event) {
if (event.date == date) {
return;
}
context
.read<CalendarBloc>()
.add(CalendarEvent.moveEvent(event, date));
},
),
NewEventButton(onCreate: () => onCreateEvent(date)),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
@@ -143,7 +142,7 @@ class _Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
@@ -166,7 +165,7 @@ class NewEventButton extends StatelessWidget {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(4.0),
child: FlowyIconButton(
onPressed: onCreate,
iconPadding: EdgeInsets.zero,
@@ -174,6 +173,35 @@ class NewEventButton extends StatelessWidget {
fillColor: Theme.of(context).colorScheme.background,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 22,
tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).brightness == Brightness.light
? const Color(0xffd0d3d6)
: const Color(0xff59647a),
width: 0.5,
),
),
borderRadius: Corners.s5Border,
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 2,
),
BoxShadow(
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 8,
),
],
),
),
);
},
@@ -195,37 +223,49 @@ class _DayBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color dayTextColor = Theme.of(context).colorScheme.onBackground;
Color monthTextColor = Theme.of(context).colorScheme.onBackground;
final String monthString =
DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
final String dayString = date.day.toString();
if (!isInMonth) {
dayTextColor = Theme.of(context).disabledColor;
monthTextColor = Theme.of(context).disabledColor;
}
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (date.day == 1) FlowyText.medium(monthString),
Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border,
),
width: isToday ? 26 : null,
height: isToday ? 26 : null,
padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
color: dayTextColor,
return SizedBox(
height: 18,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (date.day == 1)
FlowyText.medium(
monthString,
fontSize: 11,
color: monthTextColor,
),
Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: BorderRadius.circular(10),
),
width: isToday ? 18 : null,
height: isToday ? 18 : null,
// padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
fontSize: 11,
color: dayTextColor,
),
),
),
),
],
],
),
);
}
}
@@ -246,20 +286,26 @@ class _EventList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final editingEvent = context.watch<CalendarBloc>().state.editingEvent;
return Flexible(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
),
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
),
itemBuilder: (BuildContext context, int index) {
final autoEdit =
editingEvent?.event?.eventId == events[index].eventId;
return EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
autoEdit: autoEdit,
);
},
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
@@ -1,47 +1,73 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.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:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/calendar_bloc.dart';
import 'calendar_page.dart';
import 'calendar_event_editor.dart';
class EventCard extends StatelessWidget {
class EventCard extends StatefulWidget {
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
final bool autoEdit;
const EventCard({
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
required this.autoEdit,
super.key,
});
@override
State<EventCard> createState() => _EventCardState();
}
class _EventCardState extends State<EventCard> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
if (widget.autoEdit) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_popoverController.show();
context
.read<CalendarBloc>()
.add(const CalendarEvent.newEventPopupDisplayed());
});
}
}
@override
Widget build(BuildContext context) {
final rowInfo = rowCache.getRow(event.eventId);
final rowInfo = widget.rowCache.getRow(widget.event.eventId);
if (rowInfo == null) {
return const SizedBox.shrink();
}
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<CalendarDayEvent>(
rowCache.cellCache,
widget.rowCache.cellCache,
styles: styles,
);
final renderHook = _calendarEventCardRenderHook(context);
@@ -49,24 +75,22 @@ class EventCard extends StatelessWidget {
final card = RowCard<CalendarDayEvent>(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(event.eventId),
rowMeta: rowInfo!.rowMeta,
viewId: viewId,
rowCache: rowCache,
cardData: event,
key: ValueKey(widget.event.eventId),
rowMeta: rowInfo.rowMeta,
viewId: widget.viewId,
rowCache: widget.rowCache,
cardData: widget.event,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) => showEventDetails(
context: context,
event: event.event,
viewId: viewId,
rowCache: rowCache,
),
openCard: (_) => _popoverController.show(),
styleConfiguration: RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(6),
hoverStyle: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: Theme.of(context).brightness == Brightness.light
? const Color(0x0F1F2329)
: const Color(0x0FEFF4FB),
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
),
),
@@ -76,28 +100,76 @@ class EventCard extends StatelessWidget {
);
final decoration = BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
BorderSide(
color: Theme.of(context).brightness == Brightness.light
? const Color(0xffd0d3d6)
: const Color(0xff59647a),
width: 0.5,
),
),
borderRadius: Corners.s6Border,
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 2,
),
BoxShadow(
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 8,
),
],
);
return Draggable<CalendarDayEvent>(
data: event,
data: widget.event,
feedback: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth - 16.0,
maxWidth: widget.constraints.maxWidth - 8.0,
),
child: DecoratedBox(
decoration: decoration.copyWith(
color: AFThemeExtension.of(context).lightGreyHover,
child: Opacity(
opacity: 0.6,
child: DecoratedBox(
decoration: decoration,
child: card,
),
child: card,
),
),
child: DecoratedBox(
decoration: decoration,
child: card,
child: AppFlowyPopover(
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.rightWithCenterAligned,
controller: _popoverController,
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 348),
asBarrier: true,
margin: EdgeInsets.zero,
offset: const Offset(10.0, 0),
popupBuilder: (BuildContext popoverContext) {
final settings = context.watch<CalendarBloc>().state.settings.fold(
() => null,
(layoutSettings) => layoutSettings,
);
if (settings == null) {
return const SizedBox.shrink();
}
return CalendarEventEditor(
rowCache: widget.rowCache,
rowMeta: widget.event.event.rowMeta,
viewId: widget.viewId,
layoutSettings: settings,
);
},
child: DecoratedBox(
decoration: decoration,
child: card,
),
),
);
}
@@ -107,13 +179,10 @@ class EventCard extends StatelessWidget {
) {
final renderHook = RowCardRenderHook<CalendarDayEvent>();
renderHook.addTextCellHook((cellData, eventData, _) {
return BlocBuilder<TextCardCellBloc, TextCardCellState>(
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final isTitle = context
.read<TextCardCellBloc>()
.cellController
.fieldInfo
.isPrimary;
final isTitle =
context.read<TextCellBloc>().cellController.fieldInfo.isPrimary;
final text = isTitle && cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
@@ -127,8 +196,9 @@ class EventCard extends StatelessWidget {
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: null, // Enable multiple lines
fontSize: isTitle ? 11 : 10,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
@@ -0,0 +1,280 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarEventEditor extends StatelessWidget {
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
final GridCellBuilder cellBuilder;
CalendarEventEditor({
super.key,
required RowCache rowCache,
required RowMetaPB rowMeta,
required String viewId,
required this.layoutSettings,
}) : rowController = RowController(
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
),
cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
@override
Widget build(BuildContext context) {
return BlocProvider<CalendarEventEditorBloc>(
create: (context) => CalendarEventEditorBloc(
rowController: rowController,
layoutSettings: layoutSettings,
)..add(const CalendarEventEditorEvent.initial()),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
EventEditorControls(rowController: rowController),
Flexible(
child: EventPropertyList(
dateFieldId: layoutSettings.fieldId,
cellBuilder: cellBuilder,
),
),
],
),
);
}
}
class EventEditorControls extends StatelessWidget {
final RowController rowController;
const EventEditorControls({
super.key,
required this.rowController,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.delete_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context
.read<CalendarEventEditorBloc>()
.add(const CalendarEventEditorEvent.delete()),
),
const HSpace(8.0),
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.full_view_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () {
PopoverContainer.of(context).close();
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),
rowController: rowController,
);
},
);
},
),
],
),
);
}
}
class EventPropertyList extends StatelessWidget {
final String dateFieldId;
final GridCellBuilder cellBuilder;
const EventPropertyList({
super.key,
required this.dateFieldId,
required this.cellBuilder,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarEventEditorBloc, CalendarEventEditorState>(
builder: (context, state) {
final reorderedList = List<DatabaseCellContext>.from(state.cells)
..retainWhere((cell) => !cell.fieldInfo.isPrimary);
final primaryCellContext =
state.cells.firstWhereOrNull((cell) => cell.fieldInfo.isPrimary);
final dateFieldIndex =
reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId);
if (primaryCellContext == null || dateFieldIndex == -1) {
return const SizedBox.shrink();
}
reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex));
final children = <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: cellBuilder.build(
primaryCellContext,
style: GridTextCellStyle(
cellPadding: EdgeInsets.zero,
placeholder: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontSize: 11),
autofocus: true,
useRoundedBorder: true,
),
),
),
...reorderedList.map(
(cell) => PropertyCell(cellContext: cell, cellBuilder: cellBuilder),
),
];
return ListView(
shrinkWrap: true,
padding: const EdgeInsets.only(bottom: 16.0),
children: children,
);
},
);
}
}
class PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder;
const PropertyCell({
required this.cellContext,
required this.cellBuilder,
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _PropertyCellState();
}
class _PropertyCellState extends State<PropertyCell> {
@override
Widget build(BuildContext context) {
final style = _customCellStyle(widget.cellContext.fieldType);
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final gesture = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(child: cell),
);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
constraints: const BoxConstraints(minHeight: 28),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: 88,
height: 28,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6),
child: Row(
children: [
FlowySvg(
widget.cellContext.fieldType.icon(),
color: Theme.of(context).hintColor,
size: const Size.square(14),
),
const HSpace(4.0),
FlowyText.regular(
widget.cellContext.fieldInfo.name,
color: Theme.of(context).hintColor,
fontSize: 11,
),
],
),
),
),
const HSpace(8),
Expanded(child: gesture),
],
),
);
}
GridCellStyle? _customCellStyle(FieldType fieldType) {
switch (fieldType) {
case FieldType.Checkbox:
return GridCheckboxCellStyle(
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.DateTime:
return DateCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
case FieldType.MultiSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.Checklist:
return ChecklistCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.Number:
return GridNumberCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.RichText:
return GridTextCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.SingleSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.URL:
return GridURLCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
accessoryTypes: [
GridURLCellAccessoryType.copyURL,
GridURLCellAccessoryType.visitURL,
],
);
}
throw UnimplementedError;
}
}
@@ -2,11 +2,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
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/calendar/application/unschedule_event_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:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -119,19 +123,6 @@ class _CalendarPageState extends State<CalendarPage> {
);
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.editingEvent != c.editingEvent,
listener: (context, state) {
if (state.editingEvent != null) {
showEventDetails(
context: context,
event: state.editingEvent!.event!.event,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
}
},
),
BlocListener<CalendarBloc, CalendarState>(
// Event create by click the + button or double click on the
// calendar
@@ -211,40 +202,49 @@ class _CalendarPageState extends State<CalendarPage> {
}
Widget _headerNavigatorBuilder(DateTime currentMonth) {
return Row(
children: [
FlowyText.medium(
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
const Spacer(),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_left_s),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(),
),
FlowyTextButton(
LocaleKeys.calendar_navigation_today.tr(),
fillColor: Colors.transparent,
fontWeight: FontWeight.w500,
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () =>
_calendarState?.currentState?.animateToMonth(DateTime.now()),
),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_right_s),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(),
),
],
return SizedBox(
height: 24,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowyText.medium(
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
const Spacer(),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_left_s),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(),
),
FlowyTextButton(
LocaleKeys.calendar_navigation_today.tr(),
fillColor: Colors.transparent,
fontWeight: FontWeight.w400,
fontSize: 10,
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () =>
_calendarState?.currentState?.animateToMonth(DateTime.now()),
),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_right_s),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(),
),
const HSpace(6.0),
UnscheduledEventsButton(
databaseController: widget.databaseController,
),
],
),
);
}
@@ -255,8 +255,9 @@ class _CalendarPageState extends State<CalendarPage> {
return Center(
child: Padding(
padding: CalendarSize.daysOfWeekInsets,
child: FlowyText.medium(
child: FlowyText.regular(
weekDayString,
fontSize: 9,
color: Theme.of(context).hintColor,
),
),
@@ -321,3 +322,150 @@ void showEventDetails({
},
);
}
class UnscheduledEventsButton extends StatefulWidget {
final DatabaseController databaseController;
const UnscheduledEventsButton({super.key, required this.databaseController});
@override
State<UnscheduledEventsButton> createState() =>
_UnscheduledEventsButtonState();
}
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocProvider<UnscheduleEventsBloc>(
create: (_) =>
UnscheduleEventsBloc(databaseController: widget.databaseController)
..add(const UnscheduleEventsEvent.initial()),
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
builder: (context, state) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 282, maxHeight: 600),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: Corners.s6Border,
),
side:
BorderSide(color: Theme.of(context).dividerColor, width: 1),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
visualDensity: VisualDensity.compact,
),
onPressed: () {
if (state.unscheduleEvents.isNotEmpty) {
_popoverController.show();
}
},
child: FlowyText.regular(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fontSize: 10,
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: widget.databaseController.viewId,
rowCache: widget.databaseController.rowCache,
unscheduleEvents: state.unscheduleEvents,
);
},
);
},
),
);
}
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
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(),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...unscheduleEvents.map(
(e) => UnscheduledEventCell(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e,
viewId: viewId,
rowCache: rowCache,
);
PopoverContainer.of(context).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: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
fontSize: 11,
),
onTap: onPressed,
),
);
}
}
@@ -15,7 +15,7 @@ class CalendarSize {
static double get scrollBarSize => 8 * scale;
static double get navigatorButtonWidth => 20 * scale;
static double get navigatorButtonHeight => 25 * scale;
static double get navigatorButtonHeight => 24 * scale;
static EdgeInsets get daysOfWeekInsets =>
EdgeInsets.symmetric(vertical: 10.0 * scale);
EdgeInsets.only(top: 12.0 * scale, bottom: 5.0 * scale);
}
@@ -1,18 +1,6 @@
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/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';
class CalendarSettingBar extends StatelessWidget {
final DatabaseController databaseController;
@@ -28,8 +16,6 @@ class CalendarSettingBar extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
UnscheduleEventsButton(databaseController: databaseController),
const HSpace(2),
SettingButton(
databaseController: databaseController,
),
@@ -38,148 +24,3 @@ class CalendarSettingBar extends StatelessWidget {
);
}
}
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})",
fontSize: FontSizes.s11,
fontColor: AFThemeExtension.of(context).textColor,
fontWeight: FontWeight.w400,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () => _popoverController.show(),
);
},
),
),
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,
),
);
}
}
@@ -39,6 +39,8 @@ class RowBloc extends Bloc<RowEvent, RowState> {
_rowBackendSvc.createRowAfterRow(rowId);
},
didReceiveCells: (cellByFieldId, reason) async {
cellByFieldId
.removeWhere((_, cellContext) => !cellContext.isVisible());
final cells = cellByFieldId.values
.map((e) => GridCellEquatable(e.fieldInfo))
.toList();
@@ -106,16 +108,18 @@ class RowState with _$RowState {
factory RowState.initial(
CellContextByFieldId cellByFieldId,
) =>
RowState(
cellByFieldId: cellByFieldId,
cells: UnmodifiableListView(
cellByFieldId.values
.map((e) => GridCellEquatable(e.fieldInfo))
.toList(),
),
rowSource: const RowSourece.disk(),
);
) {
cellByFieldId.removeWhere((_, cellContext) => !cellContext.isVisible());
return RowState(
cellByFieldId: cellByFieldId,
cells: UnmodifiableListView(
cellByFieldId.values
.map((e) => GridCellEquatable(e.fieldInfo))
.toList(),
),
rowSource: const RowSourece.disk(),
);
}
}
class GridCellEquatable extends Equatable {
@@ -14,49 +14,73 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
RowDetailBloc({
required this.rowController,
}) : super(RowDetailState.initial()) {
}) : super(RowDetailState.initial(rowController.loadData())) {
on<RowDetailEvent>(
(event, emit) async {
await event.when(
initial: () async {
await _startListening();
final cells = rowController.loadData();
if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
}
},
didReceiveCellDatas: (cells) {
emit(state.copyWith(cells: cells));
didReceiveCellDatas: (visibleCells, allCells, numHiddenFields) {
emit(
state.copyWith(
visibleCells: visibleCells,
allCells: allCells,
numHiddenFields: numHiddenFields,
),
);
},
deleteField: (fieldId) {
_fieldBackendService(fieldId).deleteField();
final fieldService = FieldBackendService(
viewId: rowController.viewId,
fieldId: fieldId,
);
fieldService.deleteField();
},
showField: (fieldId) async {
toggleFieldVisibility: (fieldId) async {
final fieldInfo = state.allCells
.where((cellContext) => cellContext.fieldId == fieldId)
.first
.fieldInfo;
final fieldVisibility =
fieldInfo.visibility == FieldVisibility.AlwaysShown
? FieldVisibility.AlwaysHidden
: FieldVisibility.AlwaysShown;
final result =
await FieldSettingsBackendService(viewId: rowController.viewId)
.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysShown,
fieldVisibility: fieldVisibility,
);
result.fold(
(l) {},
(err) => Log.error(err),
);
},
hideField: (fieldId) async {
final result =
await FieldSettingsBackendService(viewId: rowController.viewId)
.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysHidden,
);
result.fold(
(l) {},
(err) => Log.error(err),
reorderField:
(reorderedFieldId, targetFieldId, fromIndex, toIndex) async {
await _reorderField(
reorderedFieldId,
targetFieldId,
fromIndex,
toIndex,
emit,
);
},
reorderField: (fieldId, fromIndex, toIndex) async {
await _reorderField(fieldId, fromIndex, toIndex, emit);
toggleHiddenFieldVisibility: () {
final showHiddenFields = !state.showHiddenFields;
final visibleCells = List<DatabaseCellContext>.from(state.allCells);
visibleCells.retainWhere(
(cellContext) =>
!cellContext.fieldInfo.isPrimary &&
cellContext.isVisible(showHiddenFields: showHiddenFields),
);
emit(
state.copyWith(
showHiddenFields: showHiddenFields,
visibleCells: visibleCells,
),
);
},
);
},
@@ -71,36 +95,60 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
Future<void> _startListening() async {
rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
onRowChanged: (cellMap, reason) {
if (isClosed) {
return;
}
final allCells = cellMap.values.toList();
int numHiddenFields = 0;
final visibleCells = <DatabaseCellContext>[];
for (final cell in allCells) {
final isPrimary = cell.fieldInfo.isPrimary;
if (cell.isVisible(showHiddenFields: state.showHiddenFields) &&
!isPrimary) {
visibleCells.add(cell);
}
if (!cell.isVisible() && !isPrimary) {
numHiddenFields++;
}
}
add(
RowDetailEvent.didReceiveCellDatas(
visibleCells,
allCells,
numHiddenFields,
),
);
},
);
}
FieldBackendService _fieldBackendService(String fieldId) {
return FieldBackendService(
viewId: rowController.viewId,
fieldId: fieldId,
);
}
Future<void> _reorderField(
String fieldId,
String reorderedFieldId,
String targetFieldId,
int fromIndex,
int toIndex,
Emitter<RowDetailState> emit,
) async {
final cells = List<DatabaseCellContext>.from(state.cells);
final cells = List<DatabaseCellContext>.from(state.visibleCells);
cells.insert(toIndex, cells.removeAt(fromIndex));
emit(state.copyWith(cells: cells));
emit(state.copyWith(visibleCells: cells));
final fieldService =
FieldBackendService(viewId: rowController.viewId, fieldId: fieldId);
final fromIndexInAllFields =
state.allCells.indexWhere((cell) => cell.fieldId == reorderedFieldId);
final toIndexInAllFields =
state.allCells.indexWhere((cell) => cell.fieldId == targetFieldId);
final fieldService = FieldBackendService(
viewId: rowController.viewId,
fieldId: reorderedFieldId,
);
final result = await fieldService.moveField(
fromIndex,
toIndex,
fromIndexInAllFields,
toIndexInAllFields,
);
result.fold((l) {}, (err) => Log.error(err));
}
@@ -110,25 +158,54 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
class RowDetailEvent with _$RowDetailEvent {
const factory RowDetailEvent.initial() = _Initial;
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
const factory RowDetailEvent.showField(String fieldId) = _ShowField;
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
const factory RowDetailEvent.toggleFieldVisibility(String fieldId) =
_ToggleFieldVisibility;
const factory RowDetailEvent.reorderField(
String fieldId,
String reorderFieldID,
String targetFieldID,
int fromIndex,
int toIndex,
) = _ReorderField;
const factory RowDetailEvent.toggleHiddenFieldVisibility() =
_ToggleHiddenFieldVisibility;
const factory RowDetailEvent.didReceiveCellDatas(
List<DatabaseCellContext> gridCells,
List<DatabaseCellContext> visibleCells,
List<DatabaseCellContext> allCells,
int numHiddenFields,
) = _DidReceiveCellDatas;
}
@freezed
class RowDetailState with _$RowDetailState {
const factory RowDetailState({
required List<DatabaseCellContext> cells,
required List<DatabaseCellContext> visibleCells,
required List<DatabaseCellContext> allCells,
required bool showHiddenFields,
required int numHiddenFields,
}) = _RowDetailState;
factory RowDetailState.initial() => RowDetailState(
cells: List.empty(),
);
factory RowDetailState.initial(CellContextByFieldId cellByFieldId) {
final allCells = cellByFieldId.values.toList();
int numHiddenFields = 0;
final visibleCells = <DatabaseCellContext>[];
for (final cell in allCells) {
final isVisible = cell.isVisible();
final isPrimary = cell.fieldInfo.isPrimary;
if (isVisible && !isPrimary) {
visibleCells.add(cell);
}
if (!isVisible && !isPrimary) {
numHiddenFields++;
}
}
return RowDetailState(
visibleCells: visibleCells,
allCells: allCells,
showHiddenFields: false,
numHiddenFields: numHiddenFields,
);
}
}
@@ -5,10 +5,10 @@ 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';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -410,14 +410,21 @@ class _GridFooter extends StatelessWidget {
builder: (context, rowCount) {
return Padding(
padding: GridSize.contentInsets,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
FlowyText.medium(
rowCountString(rowCount),
color: Theme.of(context).hintColor,
),
],
child: RichText(
text: TextSpan(
text: rowCountString(),
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).hintColor,
),
children: [
TextSpan(
text: ' $rowCount',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: AFThemeExtension.of(context).gridRowCountColor,
),
),
],
),
),
);
},
@@ -425,6 +432,6 @@ class _GridFooter extends StatelessWidget {
}
}
String rowCountString(int count) {
return '${LocaleKeys.grid_row_count.tr()} : $count';
String rowCountString() {
return '${LocaleKeys.grid_row_count.tr()} :';
}
@@ -40,7 +40,7 @@ class GridRowBottomBar extends StatelessWidget {
return Container(
padding: GridSize.footerContentInsets,
height: GridSize.footerHeight,
margin: const EdgeInsets.only(bottom: 200),
margin: const EdgeInsets.only(bottom: 200, top: 8),
child: const GridAddRowButton(),
);
}
@@ -38,7 +38,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return FieldCellBloc(cellContext: widget.cellContext);
return FieldCellBloc(fieldContext: widget.cellContext);
},
child: BlocBuilder<FieldCellBloc, FieldCellState>(
builder: (context, state) {
@@ -54,7 +54,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
);
},
child: FieldCellButton(
field: widget.cellContext.field,
field: widget.cellContext.fieldInfo.field,
onTap: () => popoverController.show(),
),
);
@@ -125,10 +125,15 @@ class _DragToExpandLine extends StatelessWidget {
onTap: () {},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragStart: (details) {
context
.read<FieldCellBloc>()
.add(const FieldCellEvent.onResizeStart());
},
onHorizontalDragUpdate: (value) {
context
.read<FieldCellBloc>()
.add(FieldCellEvent.startUpdateWidth(value.delta.dx));
.add(FieldCellEvent.startUpdateWidth(value.localPosition.dx));
},
onHorizontalDragEnd: (end) {
context
@@ -34,14 +34,14 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
@override
Widget build(BuildContext context) {
if (_showFieldEditor) {
final field = widget.cellContext.field;
return SizedBox(
width: 400,
child: FieldEditor(
viewId: widget.cellContext.viewId,
fieldInfo: widget.cellContext.fieldInfo,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.cellContext.viewId,
field: field,
field: widget.cellContext.fieldInfo.field,
),
),
);
@@ -96,8 +96,8 @@ class _EditFieldButton extends StatelessWidget {
}
class _FieldOperationList extends StatelessWidget {
final FieldContext fieldInfo;
const _FieldOperationList(this.fieldInfo, {Key? key}) : super(key: key);
final FieldContext fieldContext;
const _FieldOperationList(this.fieldContext, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -128,7 +128,7 @@ class _FieldOperationList extends StatelessWidget {
bool enable = true;
// If the field is primary, delete and duplicate are disabled.
if (fieldInfo.field.isPrimary) {
if (fieldContext.fieldInfo.isPrimary) {
switch (action) {
case FieldAction.hide:
break;
@@ -145,7 +145,7 @@ class _FieldOperationList extends StatelessWidget {
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FieldActionCell(
fieldInfo: fieldInfo,
fieldInfo: fieldContext,
action: action,
enable: enable,
),
@@ -217,7 +217,7 @@ extension _FieldActionExtension on FieldAction {
}
}
void run(BuildContext context, FieldContext fieldInfo) {
void run(BuildContext context, FieldContext fieldContext) {
switch (this) {
case FieldAction.hide:
context
@@ -228,8 +228,8 @@ extension _FieldActionExtension on FieldAction {
PopoverContainer.of(context).close();
FieldBackendService(
viewId: fieldInfo.viewId,
fieldId: fieldInfo.field.id,
viewId: fieldContext.viewId,
fieldId: fieldContext.fieldInfo.id,
).duplicateField();
break;
@@ -240,8 +240,8 @@ extension _FieldActionExtension on FieldAction {
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
FieldBackendService(
viewId: fieldInfo.viewId,
fieldId: fieldInfo.field.id,
viewId: fieldContext.viewId,
fieldId: fieldContext.fieldInfo.field.id,
).deleteField();
},
).show(context);
@@ -1,6 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -19,17 +21,19 @@ class FieldEditor extends StatefulWidget {
final String viewId;
final bool isGroupingField;
final Function(String)? onDeleted;
final Function(String)? onHidden;
final Function(String)? onToggleVisibility;
final FieldTypeOptionLoader typeOptionLoader;
final FieldInfo? fieldInfo;
const FieldEditor({
required this.viewId,
required this.typeOptionLoader,
this.fieldInfo,
this.isGroupingField = false,
this.onDeleted,
this.onHidden,
Key? key,
}) : super(key: key);
this.onToggleVisibility,
super.key,
});
@override
State<StatefulWidget> createState() => _FieldEditorState();
@@ -52,10 +56,15 @@ class _FieldEditorState extends State<FieldEditor> {
@override
Widget build(BuildContext context) {
final bool requireSpace = widget.onDeleted != null ||
widget.onToggleVisibility != null ||
!widget.typeOptionLoader.field.isPrimary;
final List<Widget> children = [
FieldNameTextField(popoverMutex: popoverMutex),
if (requireSpace) const VSpace(4),
if (widget.onDeleted != null) _addDeleteFieldButton(),
if (widget.onHidden != null) _addHideFieldButton(),
if (widget.onToggleVisibility != null) _addHideFieldButton(),
if (!widget.typeOptionLoader.field.isPrimary)
FieldTypeOptionCell(popoverMutex: popoverMutex),
];
@@ -83,7 +92,7 @@ class _FieldEditorState extends State<FieldEditor> {
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: _DeleteFieldButton(
child: DeleteFieldButton(
popoverMutex: popoverMutex,
onDeleted: () {
state.field.fold(
@@ -102,12 +111,14 @@ class _FieldEditorState extends State<FieldEditor> {
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: HideFieldButton(
child: FieldVisibilityToggleButton(
isFieldHidden:
widget.fieldInfo!.visibility == FieldVisibility.AlwaysHidden,
popoverMutex: popoverMutex,
onHidden: () {
onTap: () {
state.field.fold(
() => Log.error('Can not hidden the field'),
(field) => widget.onHidden?.call(field.id),
(field) => widget.onToggleVisibility?.call(field.id),
);
},
),
@@ -200,17 +211,29 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
},
);
}
@override
void dispose() {
focusNode.removeListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex.close();
}
});
focusNode.dispose();
super.dispose();
}
}
class _DeleteFieldButton extends StatelessWidget {
@visibleForTesting
class DeleteFieldButton extends StatelessWidget {
final PopoverMutex popoverMutex;
final VoidCallback? onDeleted;
const _DeleteFieldButton({
const DeleteFieldButton({
required this.popoverMutex,
required this.onDeleted,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@@ -237,15 +260,17 @@ class _DeleteFieldButton extends StatelessWidget {
}
@visibleForTesting
class HideFieldButton extends StatelessWidget {
class FieldVisibilityToggleButton extends StatelessWidget {
final bool isFieldHidden;
final PopoverMutex popoverMutex;
final VoidCallback? onHidden;
final VoidCallback? onTap;
const HideFieldButton({
const FieldVisibilityToggleButton({
required this.isFieldHidden,
required this.popoverMutex,
required this.onHidden,
Key? key,
}) : super(key: key);
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
@@ -254,10 +279,13 @@ class HideFieldButton extends StatelessWidget {
builder: (context, state) {
final Widget button = FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_field_hide.tr(),
isFieldHidden
? LocaleKeys.grid_field_show.tr()
: LocaleKeys.grid_field_hide.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.hide_s),
onTap: () => onHidden?.call(),
leftIcon:
FlowySvg(isFieldHidden ? FlowySvgs.show_m : FlowySvgs.hide_m),
onTap: onTap,
onHover: (_) => popoverMutex.close(),
);
return SizedBox(height: GridSize.popoverItemHeight, child: button);
@@ -98,12 +98,12 @@ class _GridHeaderState extends State<_GridHeader> {
.map(
(field) => FieldContext(
viewId: widget.viewId,
field: field.field,
fieldInfo: field,
),
)
.map(
(ctx) => GridFieldCell(
key: _getKeyById(ctx.field.id),
key: _getKeyById(ctx.fieldInfo.id),
cellContext: ctx,
),
)
@@ -136,7 +136,7 @@ class _GridHeaderState extends State<_GridHeader> {
int newIndex,
) {
if (cells.length > oldIndex) {
final field = cells[oldIndex].cellContext.field;
final field = cells[oldIndex].cellContext.fieldInfo.field;
context
.read<GridHeaderBloc>()
.add(GridHeaderEvent.moveField(field, oldIndex, newIndex));
@@ -73,6 +73,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
height: GridSize.popoverItemHeight,
alignment: Alignment.centerLeft,
child: FlowyText.medium(
color: Theme.of(context).colorScheme.outline,
LocaleKeys.grid_field_numberFormat.tr(),
),
);
@@ -306,7 +306,6 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: FlowyTextField(
autoClearWhenDone: true,
maxLength: 30,
text: text,
focusNode: _focusNode,
onCanceled: () {
@@ -325,4 +324,15 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
},
);
}
@override
void dispose() {
_focusNode.removeListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
_focusNode.dispose();
super.dispose();
}
}
@@ -224,7 +224,7 @@ class _SelectOptionColorCell extends StatelessWidget {
final colorIcon = SizedBox.square(
dimension: 16,
child: Container(
child: DecoratedBox(
decoration: BoxDecoration(
color: color.toColor(context),
shape: BoxShape.circle,
@@ -257,7 +257,10 @@ class RowContent extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _makeCells(context, state.cellByFieldId),
children: [
..._makeCells(context, state.cellByFieldId),
_finalCellDecoration(context),
],
),
);
},
@@ -302,6 +305,23 @@ class RowContent extends StatelessWidget {
},
).toList();
}
Widget _finalCellDecoration(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.basic,
child: Container(
width: GridSize.trailHeaderPadding,
padding: GridSize.headerContentInsets,
constraints: const BoxConstraints(minHeight: 46),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: const SizedBox.shrink(),
),
);
}
}
class RegionStateNotifier extends ChangeNotifier {
@@ -161,6 +161,7 @@ enum AddButtonAction {
case AddButtonAction.board:
return FlowySvgs.board_s;
case AddButtonAction.calendar:
return FlowySvgs.date_s;
case AddButtonAction.grid:
return FlowySvgs.grid_s;
}
@@ -1,77 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_controller_builder.dart';
part 'checkbox_card_cell_bloc.freezed.dart';
class CheckboxCardCellBloc
extends Bloc<CheckboxCardCellEvent, CheckboxCardCellState> {
final CheckboxCellController cellController;
void Function()? _onCellChangedFn;
CheckboxCardCellBloc({
required this.cellController,
}) : super(CheckboxCardCellState.initial(cellController)) {
on<CheckboxCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
},
select: () async {
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(CheckboxCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class CheckboxCardCellEvent with _$CheckboxCardCellEvent {
const factory CheckboxCardCellEvent.initial() = _InitialCell;
const factory CheckboxCardCellEvent.select() = _Selected;
const factory CheckboxCardCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class CheckboxCardCellState with _$CheckboxCardCellState {
const factory CheckboxCardCellState({
required bool isSelected,
}) = _CheckboxCellState;
factory CheckboxCardCellState.initial(TextCellController context) {
return CheckboxCardCellState(
isSelected: _isSelected(context.getCellData()),
);
}
}
bool _isSelected(String? cellData) {
// The backend use "Yes" and "No" to represent the checkbox cell data.
return cellData == "Yes";
}
@@ -1,90 +0,0 @@
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_controller_builder.dart';
part 'date_card_cell_bloc.freezed.dart';
class DateCardCellBloc extends Bloc<DateCardCellEvent, DateCardCellState> {
final DateCellController cellController;
void Function()? _onCellChangedFn;
DateCardCellBloc({required this.cellController})
: super(DateCardCellState.initial(cellController)) {
on<DateCardCellEvent>(
(event, emit) async {
event.when(
initial: () => _startListening(),
didReceiveCellUpdate: (DateCellDataPB? cellData) {
emit(
state.copyWith(
data: cellData,
dateStr: _dateStrFromCellData(cellData),
),
);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((data) {
if (!isClosed) {
add(DateCardCellEvent.didReceiveCellUpdate(data));
}
}),
);
}
}
@freezed
class DateCardCellEvent with _$DateCardCellEvent {
const factory DateCardCellEvent.initial() = _InitialCell;
const factory DateCardCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate;
}
@freezed
class DateCardCellState with _$DateCardCellState {
const factory DateCardCellState({
required DateCellDataPB? data,
required String dateStr,
required FieldInfo fieldInfo,
}) = _DateCardCellState;
factory DateCardCellState.initial(DateCellController context) {
final cellData = context.getCellData();
return DateCardCellState(
fieldInfo: context.fieldInfo,
data: cellData,
dateStr: _dateStrFromCellData(cellData),
);
}
}
String _dateStrFromCellData(DateCellDataPB? cellData) {
String dateStr = "";
if (cellData != null) {
if (cellData.includeTime) {
dateStr = '${cellData.date} ${cellData.time}';
} else {
dateStr = cellData.date;
}
}
return dateStr;
}
@@ -1,68 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_controller_builder.dart';
part 'number_card_cell_bloc.freezed.dart';
class NumberCardCellBloc
extends Bloc<NumberCardCellEvent, NumberCardCellState> {
final NumberCellController cellController;
void Function()? _onCellChangedFn;
NumberCardCellBloc({
required this.cellController,
}) : super(NumberCardCellState.initial(cellController)) {
on<NumberCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(NumberCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class NumberCardCellEvent with _$NumberCardCellEvent {
const factory NumberCardCellEvent.initial() = _InitialCell;
const factory NumberCardCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class NumberCardCellState with _$NumberCardCellState {
const factory NumberCardCellState({
required String content,
}) = _NumberCardCellState;
factory NumberCardCellState.initial(TextCellController context) =>
NumberCardCellState(
content: context.getCellData() ?? "",
);
}
@@ -1,78 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'select_option_card_cell_bloc.freezed.dart';
class SelectOptionCardCellBloc
extends Bloc<SelectOptionCardCellEvent, SelectOptionCardCellState> {
final SelectOptionCellController cellController;
void Function()? _onCellChangedFn;
SelectOptionCardCellBloc({
required this.cellController,
}) : super(SelectOptionCardCellState.initial(cellController)) {
on<SelectOptionCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveOptions: (List<SelectOptionPB> selectedOptions) {
emit(state.copyWith(selectedOptions: selectedOptions));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((selectOptionContext) {
if (!isClosed) {
add(
SelectOptionCardCellEvent.didReceiveOptions(
selectOptionContext?.selectOptions ?? [],
),
);
}
}),
);
}
}
@freezed
class SelectOptionCardCellEvent with _$SelectOptionCardCellEvent {
const factory SelectOptionCardCellEvent.initial() = _InitialCell;
const factory SelectOptionCardCellEvent.didReceiveOptions(
List<SelectOptionPB> selectedOptions,
) = _DidReceiveOptions;
}
@freezed
class SelectOptionCardCellState with _$SelectOptionCardCellState {
const factory SelectOptionCardCellState({
required List<SelectOptionPB> selectedOptions,
}) = _SelectOptionCardCellState;
factory SelectOptionCardCellState.initial(
SelectOptionCellController context,
) {
final data = context.getCellData();
return SelectOptionCardCellState(
selectedOptions: data?.selectOptions ?? [],
);
}
}
@@ -1,79 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'text_card_cell_bloc.freezed.dart';
class TextCardCellBloc extends Bloc<TextCardCellEvent, TextCardCellState> {
final TextCellController cellController;
void Function()? _onCellChangedFn;
TextCardCellBloc({
required this.cellController,
}) : super(TextCardCellState.initial(cellController)) {
on<TextCardCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
updateText: (text) {
if (text != state.content) {
cellController.saveCellData(text);
emit(state.copyWith(content: text));
}
},
enableEdit: (bool enabled) {
emit(state.copyWith(enableEdit: enabled));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(TextCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class TextCardCellEvent with _$TextCardCellEvent {
const factory TextCardCellEvent.initial() = _InitialCell;
const factory TextCardCellEvent.updateText(String text) = _UpdateContent;
const factory TextCardCellEvent.enableEdit(bool enabled) = _EnableEdit;
const factory TextCardCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class TextCardCellState with _$TextCardCellState {
const factory TextCardCellState({
required String content,
required bool enableEdit,
}) = _TextCardCellState;
factory TextCardCellState.initial(TextCellController context) =>
TextCardCellState(
content: context.getCellData() ?? "",
enableEdit: false,
);
}
@@ -1,80 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'timestamp_card_cell_bloc.freezed.dart';
class TimestampCardCellBloc
extends Bloc<TimestampCardCellEvent, TimestampCardCellState> {
final TimestampCellController cellController;
void Function()? _onCellChangedFn;
TimestampCardCellBloc({required this.cellController})
: super(TimestampCardCellState.initial(cellController)) {
on<TimestampCardCellEvent>(
(event, emit) async {
event.when(
initial: () => _startListening(),
didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
emit(
state.copyWith(
data: cellData,
dateStr: cellData?.dateTime ?? "",
),
);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((data) {
if (!isClosed) {
add(TimestampCardCellEvent.didReceiveCellUpdate(data));
}
}),
);
}
}
@freezed
class TimestampCardCellEvent with _$TimestampCardCellEvent {
const factory TimestampCardCellEvent.initial() = _InitialCell;
const factory TimestampCardCellEvent.didReceiveCellUpdate(
TimestampCellDataPB? data,
) = _DidReceiveCellUpdate;
}
@freezed
class TimestampCardCellState with _$TimestampCardCellState {
const factory TimestampCardCellState({
required TimestampCellDataPB? data,
required String dateStr,
required FieldInfo fieldInfo,
}) = _TimestampCardCellState;
factory TimestampCardCellState.initial(TimestampCellController context) {
final cellData = context.getCellData();
return TimestampCardCellState(
fieldInfo: context.fieldInfo,
data: cellData,
dateStr: cellData?.dateTime ?? "",
);
}
}
@@ -1,80 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'url_card_cell_bloc.freezed.dart';
class URLCardCellBloc extends Bloc<URLCardCellEvent, URLCardCellState> {
final URLCellController cellController;
void Function()? _onCellChangedFn;
URLCardCellBloc({
required this.cellController,
}) : super(URLCardCellState.initial(cellController)) {
on<URLCardCellEvent>(
(event, emit) async {
event.when(
initial: () {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(
state.copyWith(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
),
);
},
updateURL: (String url) {
cellController.saveCellData(url, deduplicate: true);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
await cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellData) {
if (!isClosed) {
add(URLCardCellEvent.didReceiveCellUpdate(cellData));
}
}),
);
}
}
@freezed
class URLCardCellEvent with _$URLCardCellEvent {
const factory URLCardCellEvent.initial() = _InitialCell;
const factory URLCardCellEvent.updateURL(String url) = _UpdateURL;
const factory URLCardCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
_DidReceiveCellUpdate;
}
@freezed
class URLCardCellState with _$URLCardCellState {
const factory URLCardCellState({
required String content,
required String url,
}) = _URLCardCellState;
factory URLCardCellState.initial(URLCellController context) {
final cellData = context.getCellData();
return URLCardCellState(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
);
}
}
@@ -1,68 +0,0 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/number_card_cell_bloc.dart';
import 'define.dart';
class BoardNumberCell extends StatefulWidget {
final String groupId;
final CellControllerBuilder cellControllerBuilder;
const BoardNumberCell({
required this.groupId,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@override
State<BoardNumberCell> createState() => _NumberCardCellState();
}
class _NumberCardCellState extends State<BoardNumberCell> {
late NumberCardCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCardCellBloc(cellController: cellController)
..add(const NumberCardCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(
vertical: CardSizes.cardCellVPadding,
),
child: FlowyText.medium(
state.content,
fontSize: 14,
),
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}
@@ -93,6 +93,8 @@ List<DatabaseCellContext> _makeCells(
CellContextByFieldId originalCellMap,
) {
final List<DatabaseCellContext> cells = [];
originalCellMap
.removeWhere((fieldId, cellContext) => !cellContext.isVisible());
for (final entry in originalCellMap.entries) {
// Filter out the cell if it's fieldId equal to the groupFieldId
if (groupFieldId != null) {
@@ -1,11 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/checkbox_card_cell_bloc.dart';
import 'card_cell.dart';
class CheckboxCardCell extends CardCell {
@@ -17,18 +16,18 @@ class CheckboxCardCell extends CardCell {
}) : super(key: key);
@override
State<CheckboxCardCell> createState() => _CheckboxCardCellState();
State<CheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCardCellState extends State<CheckboxCardCell> {
late CheckboxCardCellBloc _cellBloc;
class _CheckboxCellState extends State<CheckboxCardCell> {
late CheckboxCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCardCellBloc(cellController: cellController);
_cellBloc.add(const CheckboxCardCellEvent.initial());
_cellBloc = CheckboxCellBloc(cellController: cellController);
_cellBloc.add(const CheckboxCellEvent.initial());
super.initState();
}
@@ -36,7 +35,7 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCardCellBloc, CheckboxCardCellState>(
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
@@ -55,8 +54,8 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
icon: icon,
width: 20,
onPressed: () => context
.read<CheckboxCardCellBloc>()
.add(const CheckboxCardCellEvent.select()),
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
),
);
@@ -12,17 +12,17 @@ class ChecklistCardCell extends CardCell {
: super(key: key);
@override
State<ChecklistCardCell> createState() => _ChecklistCardCellState();
State<ChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCardCellState extends State<ChecklistCardCell> {
late ChecklistCardCellBloc _cellBloc;
class _ChecklistCellState extends State<ChecklistCardCell> {
late ChecklistCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCardCellBloc(cellController: cellController);
_cellBloc = ChecklistCellBloc(cellController: cellController);
_cellBloc.add(const ChecklistCellEvent.initial());
super.initState();
}
@@ -31,7 +31,7 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.allOptions.isEmpty) {
return const SizedBox.shrink();
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/date_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -18,19 +18,19 @@ class DateCardCell<CustomCardData> extends CardCell {
}) : super(key: key);
@override
State<DateCardCell> createState() => _DateCardCellState();
State<DateCardCell> createState() => _DateCellState();
}
class _DateCardCellState extends State<DateCardCell> {
late DateCardCellBloc _cellBloc;
class _DateCellState extends State<DateCardCell> {
late DateCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCardCellBloc(cellController: cellController)
..add(const DateCardCellEvent.initial());
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
super.initState();
}
@@ -38,7 +38,7 @@ class _DateCardCellState extends State<DateCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCardCellBloc, DateCardCellState>(
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/number_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -27,19 +27,19 @@ class NumberCardCell<CustomCardData>
}) : super(key: key, style: style, cardData: cardData);
@override
State<NumberCardCell> createState() => _NumberCardCellState();
State<NumberCardCell> createState() => _NumberCellState();
}
class _NumberCardCellState extends State<NumberCardCell> {
late NumberCardCellBloc _cellBloc;
class _NumberCellState extends State<NumberCardCell> {
late NumberCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCardCellBloc(cellController: cellController)
..add(const NumberCardCellEvent.initial());
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
super.initState();
}
@@ -47,14 +47,15 @@ class _NumberCardCellState extends State<NumberCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
buildWhen: (previous, current) => previous.content != current.content,
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) =>
previous.cellContent != current.cellContent,
builder: (context, state) {
if (state.content.isEmpty) {
if (state.cellContent.isEmpty) {
return const SizedBox();
} else {
final Widget? custom = widget.renderHook?.call(
state.content,
state.cellContent,
widget.cardData,
context,
);
@@ -69,7 +70,7 @@ class _NumberCardCellState extends State<NumberCardCell> {
vertical: CardSizes.cardCellVPadding,
),
child: FlowyText.medium(
state.content,
state.cellContent,
fontSize: widget.style?.fontSize ?? 14,
),
),
@@ -1,12 +1,13 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/select_option_card_cell_bloc.dart';
import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {}
@@ -29,11 +30,11 @@ class SelectOptionCardCell<CustomCardData>
}) : super(key: key, cardData: cardData);
@override
State<SelectOptionCardCell> createState() => _SelectOptionCardCellState();
State<SelectOptionCardCell> createState() => _SelectOptionCellState();
}
class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
late SelectOptionCardCellBloc _cellBloc;
class _SelectOptionCellState extends State<SelectOptionCardCell> {
late SelectOptionCellBloc _cellBloc;
late PopoverController _popover;
@override
@@ -41,8 +42,8 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
_popover = PopoverController();
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCardCellBloc(cellController: cellController)
..add(const SelectOptionCardCellEvent.initial());
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
super.initState();
}
@@ -50,7 +51,7 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<SelectOptionCardCellBloc, SelectOptionCardCellState>(
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../row/cell_builder.dart';
import '../bloc/text_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -30,11 +30,11 @@ class TextCardCell<CustomCardData>
}) : super(key: key, style: style, cardData: cardData);
@override
State<TextCardCell> createState() => _TextCardCellState();
State<TextCardCell> createState() => _TextCellState();
}
class _TextCardCellState extends State<TextCardCell> {
late TextCardCellBloc _cellBloc;
class _TextCellState extends State<TextCardCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
bool focusWhenInit = false;
final focusNode = SingleListenerFocusNode();
@@ -43,8 +43,8 @@ class _TextCardCellState extends State<TextCardCell> {
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCardCellBloc(cellController: cellController)
..add(const TextCardCellEvent.initial());
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
if (focusWhenInit) {
@@ -58,7 +58,7 @@ class _TextCardCellState extends State<TextCardCell> {
if (!focusNode.hasFocus) {
focusWhenInit = false;
widget.editableNotifier?.isCellEditing.value = false;
_cellBloc.add(const TextCardCellEvent.enableEdit(false));
_cellBloc.add(const TextCellEvent.enableEdit(false));
}
});
_bindEditableNotifier();
@@ -75,7 +75,7 @@ class _TextCardCellState extends State<TextCardCell> {
focusNode.requestFocus();
});
}
_cellBloc.add(TextCardCellEvent.enableEdit(isEditing));
_cellBloc.add(TextCellEvent.enableEdit(isEditing));
});
}
@@ -89,13 +89,13 @@ class _TextCardCellState extends State<TextCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCardCellBloc, TextCardCellState>(
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: BlocBuilder<TextCardCellBloc, TextCardCellState>(
child: BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (previous, current) {
if (previous.content != current.content &&
_controller.text == current.content &&
@@ -137,7 +137,7 @@ class _TextCardCellState extends State<TextCardCell> {
}
Future<void> focusChanged() async {
_cellBloc.add(TextCardCellEvent.updateText(_controller.text));
_cellBloc.add(TextCellEvent.updateText(_controller.text));
}
@override
@@ -156,7 +156,7 @@ class _TextCardCellState extends State<TextCardCell> {
}
}
Widget _buildText(TextCardCellState state) {
Widget _buildText(TextCellState state) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: CardSizes.cardCellVPadding,
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -18,19 +18,19 @@ class TimestampCardCell<CustomCardData> extends CardCell {
}) : super(key: key);
@override
State<TimestampCardCell> createState() => _TimestampCardCellState();
State<TimestampCardCell> createState() => _TimestampCellState();
}
class _TimestampCardCellState extends State<TimestampCardCell> {
late TimestampCardCellBloc _cellBloc;
class _TimestampCellState extends State<TimestampCardCell> {
late TimestampCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCardCellBloc(cellController: cellController)
..add(const TimestampCardCellEvent.initial());
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
super.initState();
}
@@ -38,7 +38,7 @@ class _TimestampCardCellState extends State<TimestampCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCardCellBloc, TimestampCardCellState>(
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
@@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:flowy_infra/size.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/url_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
@@ -24,18 +24,18 @@ class URLCardCell<CustomCardData>
}) : super(key: key, style: style);
@override
State<URLCardCell> createState() => _URLCardCellState();
State<URLCardCell> createState() => _URLCellState();
}
class _URLCardCellState extends State<URLCardCell> {
late URLCardCellBloc _cellBloc;
class _URLCellState extends State<URLCardCell> {
late URLCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCardCellBloc(cellController: cellController);
_cellBloc.add(const URLCardCellEvent.initial());
_cellBloc = URLCellBloc(cellController: cellController);
_cellBloc.add(const URLCellEvent.initial());
super.initState();
}
@@ -43,7 +43,7 @@ class _URLCardCellState extends State<URLCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<URLCardCellBloc, URLCardCellState>(
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
@@ -66,7 +66,10 @@ class CardAccessoryContainer extends StatelessWidget {
return Container(
clipBehavior: Clip.hardEdge,
decoration: decoration,
child: child,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: child,
),
);
}
}
@@ -143,7 +143,7 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
),
);
},
icon: visibleIcon.padding(all: 6.0),
icon: visibleIcon.padding(all: 4.0),
),
onTap: () => _popoverController.show(),
).padding(horizontal: 6.0),
@@ -151,6 +151,7 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
popupBuilder: (BuildContext context) {
return FieldEditor(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.fieldInfo.field,
@@ -3,6 +3,7 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -66,7 +67,7 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
return Tooltip(
return FlowyTooltip.delayed(
message: LocaleKeys.tooltip_openAsPage.tr(),
child: SizedBox(
width: 26,
@@ -67,6 +67,7 @@ class GridCellBuilder {
case FieldType.Checklist:
return GridChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Number:
@@ -246,7 +247,9 @@ class RequestFocusListener extends ChangeNotifier {
}
}
abstract class GridCellStyle {}
abstract class GridCellStyle {
const GridCellStyle();
}
class SingleListenerFocusNode extends FocusNode {
VoidCallback? _listener;
@@ -4,4 +4,5 @@ export 'date_cell/date_cell.dart';
export 'number_cell/number_cell.dart';
export 'select_option_cell/select_option_cell.dart';
export 'text_cell/text_cell.dart';
export 'timestamp_cell/timestamp_cell.dart';
export 'url_cell/url_cell.dart';
@@ -1,13 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'checkbox_cell_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridCheckboxCellStyle extends GridCellStyle {
EdgeInsets? cellPadding;
@@ -44,10 +43,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
void initState() {
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(
service: CellBackendService(),
cellController: cellController,
)..add(const CheckboxCellEvent.initial());
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
super.initState();
}
@@ -1,5 +1,4 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@@ -11,7 +10,6 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
void Function()? _onCellChangedFn;
CheckboxCellBloc({
required CellBackendService service,
required this.cellController,
}) : super(CheckboxCellState.initial(cellController)) {
on<CheckboxCellEvent>(
@@ -20,12 +18,12 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
initial: () {
_startListening();
},
select: () async {
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
},
select: () async {
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
},
);
},
);
@@ -73,5 +71,6 @@ class CheckboxCellState with _$CheckboxCellState {
}
bool _isSelected(String? cellData) {
// The backend use "Yes" and "No" to represent the checkbox cell data.
return cellData == "Yes";
}
@@ -10,17 +10,33 @@ import 'checklist_cell_bloc.dart';
import 'checklist_cell_editor.dart';
import 'checklist_progress_bar.dart';
class ChecklistCellStyle extends GridCellStyle {
String placeholder;
EdgeInsets? cellPadding;
ChecklistCellStyle({
required this.placeholder,
this.cellPadding,
});
}
class GridChecklistCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
GridChecklistCell({required this.cellControllerBuilder, Key? key})
: super(key: key);
late final ChecklistCellStyle? cellStyle;
GridChecklistCell({
required this.cellControllerBuilder,
GridCellStyle? style,
super.key,
}) {
cellStyle = style as ChecklistCellStyle?;
}
@override
GridCellState<GridChecklistCell> createState() => GridChecklistCellState();
}
class GridChecklistCellState extends GridCellState<GridChecklistCell> {
late ChecklistCardCellBloc _cellBloc;
late ChecklistCellBloc _cellBloc;
late final PopoverController _popover;
@override
@@ -28,7 +44,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
_popover = PopoverController();
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCardCellBloc(cellController: cellController);
_cellBloc = ChecklistCellBloc(cellController: cellController);
_cellBloc.add(const ChecklistCellEvent.initial());
super.initState();
}
@@ -53,15 +69,22 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
);
},
onClose: () => widget.onCellFocus.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.allOptions.isEmpty) {
return const SizedBox.shrink();
}
return ChecklistProgressBar(percent: state.percent);
},
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding:
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.allOptions.isEmpty) {
return FlowyText.medium(
widget.cellStyle?.placeholder ?? "",
color: Theme.of(context).hintColor,
);
}
return ChecklistProgressBar(percent: state.percent);
},
),
),
),
),
@@ -8,12 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'checklist_cell_bloc.freezed.dart';
class ChecklistCardCellBloc
extends Bloc<ChecklistCellEvent, ChecklistCellState> {
class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
final ChecklistCellController cellController;
final ChecklistCellBackendService _checklistCellSvc;
void Function()? _onCellChangedFn;
ChecklistCardCellBloc({
ChecklistCellBloc({
required this.cellController,
}) : _checklistCellSvc = ChecklistCellBackendService(
viewId: cellController.viewId,
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
@@ -30,13 +32,19 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
/// Focus node for the new task text field
late final FocusNode newTaskFocusNode;
/// A flag that determines whether the new task text field is visible
bool _isAddingNewTask = false;
@override
void initState() {
super.initState();
newTaskFocusNode = FocusNode();
newTaskFocusNode = FocusNode(
onKey: (node, event) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
node.unfocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
);
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
..add(const ChecklistCellEditorEvent.initial());
}
@@ -48,59 +56,35 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
listener: (context, state) {
if (state.allOptions.isEmpty) {
setState(() => _isAddingNewTask = true);
newTaskFocusNode.requestFocus();
}
},
builder: (context, state) {
return Focus(
onKey: (node, event) {
// don't hide new task text field if there are no tasks at all
if (state.allOptions.isNotEmpty &&
event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
setState(() {
_isAddingNewTask = false;
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: CustomScrollView(
shrinkWrap: true,
physics: StyledScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.allOptions.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: ChecklistProgressBar(
percent: state.percent,
),
),
),
),
ChecklistItemList(
options: state.allOptions,
newTaskFocusNode: newTaskFocusNode,
isAddingNewTask: _isAddingNewTask,
onUpdateTask: () => setState(() {
_isAddingNewTask = true;
newTaskFocusNode.requestFocus();
}),
),
const SliverToBoxAdapter(
child: TypeOptionSeparator(spacing: 0.0),
),
SliverToBoxAdapter(
child: ChecklistNewTaskButton(
onTap: () => setState(() => _isAddingNewTask = true),
),
),
],
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.allOptions.isEmpty
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: ChecklistProgressBar(
percent: state.percent,
),
),
),
ChecklistItemList(
options: state.allOptions,
onUpdateTask: () => newTaskFocusNode.requestFocus(),
),
if (state.allOptions.isNotEmpty)
const TypeOptionSeparator(spacing: 0.0),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: NewTaskItem(focusNode: newTaskFocusNode),
),
],
);
},
),
@@ -118,16 +102,12 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
/// a new task if `isAddingNewTask` is true
class ChecklistItemList extends StatefulWidget {
final List<ChecklistSelectOption> options;
final FocusNode newTaskFocusNode;
final bool isAddingNewTask;
final VoidCallback onUpdateTask;
const ChecklistItemList({
super.key,
required this.options,
required this.onUpdateTask,
required this.isAddingNewTask,
required this.newTaskFocusNode,
});
@override
@@ -137,32 +117,28 @@ class ChecklistItemList extends StatefulWidget {
class _ChecklistItemListState extends State<ChecklistItemList> {
@override
Widget build(BuildContext context) {
final itemList = [
const VSpace(6.0),
...widget.options.mapIndexed(
(index, option) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: ChecklistItem(
if (widget.options.isEmpty) {
return const SizedBox.shrink();
}
final itemList = widget.options
.mapIndexed(
(index, option) => ChecklistItem(
option: option,
onSubmitted:
index == widget.options.length - 1 ? widget.onUpdateTask : null,
key: ValueKey(option.data.id),
// only allow calling the callback for the last task in the list
),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: widget.isAddingNewTask
? NewTaskItem(focusNode: widget.newTaskFocusNode)
: const SizedBox.shrink(),
),
const VSpace(6.0),
];
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => itemList[index],
childCount: itemList.length,
)
.toList();
return Flexible(
child: ListView.separated(
itemBuilder: (context, index) => itemList[index],
separatorBuilder: (context, index) => const VSpace(4),
itemCount: itemList.length,
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8.0),
),
);
}
@@ -187,6 +163,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
late final TextEditingController _textController;
late final FocusNode _focusNode;
bool _isHovered = false;
Timer? _debounceOnChanged;
@override
void initState() {
@@ -249,13 +226,9 @@ class _ChecklistItemState extends State<ChecklistItem> {
),
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
),
onSubmitted: (taskDescription) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.updateTaskName(
widget.option.data,
taskDescription,
),
);
onChanged: _debounceOnChangedText,
onSubmitted: (description) {
_submitUpdateTaskDescription(description);
widget.onSubmitted?.call();
},
),
@@ -276,6 +249,22 @@ class _ChecklistItemState extends State<ChecklistItem> {
),
);
}
void _debounceOnChangedText(String text) {
_debounceOnChanged?.cancel();
_debounceOnChanged = Timer(const Duration(milliseconds: 300), () {
_submitUpdateTaskDescription(text);
});
}
void _submitUpdateTaskDescription(String description) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.updateTaskName(
widget.option.data,
description,
),
);
}
}
/// Creates a new task after entering the description and pressing enter.
@@ -304,19 +293,12 @@ class _NewTaskItemState extends State<NewTaskItem> {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 8),
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const FlowyIconButton(
width: 32,
icon: FlowySvg(
FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
),
hoverColor: Colors.transparent,
),
const HSpace(8),
Expanded(
child: TextField(
focusNode: widget.focusNode,
@@ -330,7 +312,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
vertical: 6.0,
horizontal: 2.0,
),
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
hintText: LocaleKeys.grid_checklist_addNew.tr(),
),
onSubmitted: (taskDescription) {
if (taskDescription.trim().isNotEmpty) {
@@ -340,15 +322,21 @@ class _NewTaskItemState extends State<NewTaskItem> {
),
);
}
widget.focusNode.requestFocus();
_textEditingController.clear();
},
onChanged: (value) => setState(() {}),
),
),
FlowyTextButton(
LocaleKeys.grid_checklist_submitNewTask.tr(),
fontSize: 11,
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: Theme.of(context).colorScheme.primaryContainer,
fillColor: _textEditingController.text.isEmpty
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.primary,
hoverColor: _textEditingController.text.isEmpty
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.primaryContainer,
fontColor: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
onPressed: () {
@@ -359,6 +347,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
),
);
}
widget.focusNode.requestFocus();
_textEditingController.clear();
},
),
@@ -367,25 +356,3 @@ class _NewTaskItemState extends State<NewTaskItem> {
);
}
}
@visibleForTesting
class ChecklistNewTaskButton extends StatelessWidget {
final VoidCallback onTap;
const ChecklistNewTaskButton({super.key, required this.onTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: SizedBox(
height: 30,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_checklist_addNew.tr()),
margin: const EdgeInsets.all(6),
leftIcon: const FlowySvg(FlowySvgs.add_s),
onTap: onTap,
),
),
);
}
}
@@ -10,10 +10,10 @@ import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:easy_localization/easy_localization.dart'
show StringTranslateExtension;
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
import 'package:table_calendar/table_calendar.dart';
part 'date_cal_bloc.freezed.dart';
@@ -38,40 +38,75 @@ class DateCellCalendarBloc
await event.when(
initial: () async => _startListening(),
didReceiveCellUpdate: (DateCellDataPB? cellData) {
final (dateTime, time, includeTime) =
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
_dateDataFromCellData(cellData);
final endDay =
isRange == state.isRange && isRange ? endDateTime : null;
emit(
state.copyWith(
dateTime: dateTime,
time: time,
endDateTime: endDateTime,
endTime: endTime,
includeTime: includeTime,
isRange: isRange,
startDay: isRange ? dateTime : null,
endDay: endDay,
),
);
},
didReceiveTimeFormatError: (String? timeFormatError) {
emit(state.copyWith(timeFormatError: timeFormatError));
didReceiveTimeFormatError:
(String? parseTimeError, String? parseEndTimeError) {
emit(
state.copyWith(
parseTimeError: parseTimeError,
parseEndTimeError: parseEndTimeError,
),
);
},
selectDay: (date) async {
if (state.isRange) {
return;
}
await _updateDateData(date: date);
},
setIncludeTime: (includeTime) async {
await _updateDateData(includeTime: includeTime);
},
setIsRange: (isRange) async {
await _updateDateData(isRange: isRange);
},
setTime: (time) async {
await _updateDateData(time: time);
},
selectDateRange: (DateTime? start, DateTime? end) async {
if (end == null && state.startDay != null && state.endDay == null) {
final (newStart, newEnd) = state.startDay!.isBefore(start!)
? (state.startDay!, start)
: (start, state.startDay!);
emit(state.copyWith(startDay: null, endDay: null));
await _updateDateData(
date: newStart.date,
endDate: newEnd.date,
);
} else if (end == null) {
emit(state.copyWith(startDay: start, endDay: null));
} else {
await _updateDateData(
date: start!.date,
endDate: end.date,
);
}
},
setEndTime: (String endTime) async {
await _updateDateData(endTime: endTime);
},
setDateFormat: (dateFormat) async {
await _updateTypeOption(emit, dateFormat: dateFormat);
},
setTimeFormat: (timeFormat) async {
await _updateTypeOption(emit, timeFormat: timeFormat);
},
setCalFormat: (format) {
emit(state.copyWith(format: format));
},
setFocusedDay: (focusedDay) {
emit(state.copyWith(focusedDay: focusedDay));
},
clearDate: () async {
await _clearDate();
},
@@ -83,39 +118,66 @@ class DateCellCalendarBloc
Future<void> _updateDateData({
DateTime? date,
String? time,
DateTime? endDate,
String? endTime,
bool? includeTime,
bool? isRange,
}) async {
// make sure that not both date and time are updated at the same time
assert(
date == null && time == null ||
date == null && time != null ||
date != null && time == null,
!(date != null && time != null) || !(endDate != null && endTime != null),
);
// if not updating the time, use the old time in the state
final String? newTime = time ?? state.time;
DateTime? newDate = _utcToLocalAndAddCurrentTime(date);
DateTime? newDate;
if (time != null && time.isNotEmpty) {
newDate = state.dateTime ?? DateTime.now();
} else {
newDate = _utcToLocalAndAddCurrentTime(date);
}
// if not updating the time, use the old time in the state
final String? newEndTime = endTime ?? state.endTime;
DateTime? newEndDate;
if (endTime != null && endTime.isNotEmpty) {
newEndDate = state.endDateTime ?? DateTime.now();
} else {
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
}
final result = await _dateCellBackendService.update(
date: newDate,
time: newTime,
endDate: newEndDate,
endTime: newEndTime,
includeTime: includeTime ?? state.includeTime,
isRange: isRange ?? state.isRange,
);
result.fold(
(_) {
if (!isClosed && state.timeFormatError != null) {
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
if (!isClosed &&
(state.parseEndTimeError != null || state.parseTimeError != null)) {
add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
);
}
},
(err) {
switch (err.code) {
case ErrorCode.InvalidDateTimeFormat:
if (isClosed) return;
if (isClosed) {
return;
}
// to determine which textfield should show error
final (startError, endError) = newDate != null
? (timeFormatPrompt(err), null)
: (null, timeFormatPrompt(err));
add(
DateCellCalendarEvent.didReceiveTimeFormatError(
timeFormatPrompt(err),
startError,
endError,
),
);
break;
@@ -130,9 +192,13 @@ class DateCellCalendarBloc
final result = await _dateCellBackendService.clear();
result.fold(
(_) {
if (!isClosed) {
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
if (isClosed) {
return;
}
add(
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
);
},
(err) => Log.error(err),
);
@@ -157,18 +223,13 @@ class DateCellCalendarBloc
}
String timeFormatPrompt(FlowyError error) {
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
switch (state.dateTypeOptionPB.timeFormat) {
case TimeFormatPB.TwelveHour:
msg = "$msg e.g. 01:00 PM";
break;
case TimeFormatPB.TwentyFourHour:
msg = "$msg e.g. 13:00";
break;
default:
break;
}
return msg;
return switch (state.dateTypeOptionPB.timeFormat) {
TimeFormatPB.TwelveHour =>
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 01:00 PM",
TimeFormatPB.TwentyFourHour =>
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 13:00",
_ => "",
};
}
@override
@@ -235,19 +296,21 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
DateCellDataPB? data,
) = _DidReceiveCellUpdate;
const factory DateCellCalendarEvent.didReceiveTimeFormatError(
String? timeformatError,
String? parseTimeError,
String? parseEndTimeError,
) = _DidReceiveTimeFormatError;
// table calendar's UI settings
const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay;
const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) =
_CalendarFormat;
// date cell data is modified
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellCalendarEvent.selectDateRange(
DateTime? start,
DateTime? end,
) = _SelectDateRange;
const factory DateCellCalendarEvent.setTime(String time) = _Time;
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime;
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
_IncludeTime;
const factory DateCellCalendarEvent.setIsRange(bool isRange) = _IsRange;
// date field type options are modified
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) =
@@ -261,13 +324,24 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
@freezed
class DateCellCalendarState with _$DateCellCalendarState {
const factory DateCellCalendarState({
// the date field's type option
required DateTypeOptionPB dateTypeOptionPB,
required CalendarFormat format,
required DateTime focusedDay,
// used when selecting a date range
required DateTime? startDay,
required DateTime? endDay,
// cell data from the backend
required DateTime? dateTime,
required DateTime? endDateTime,
required String? time,
required String? endTime,
required bool includeTime,
required String? timeFormatError,
required bool isRange,
// error and hint text
required String? parseTimeError,
required String? parseEndTimeError,
required String timeHintText,
}) = _DateCellCalendarState;
@@ -275,15 +349,20 @@ class DateCellCalendarState with _$DateCellCalendarState {
DateTypeOptionPB dateTypeOptionPB,
DateCellDataPB? cellData,
) {
final (dateTime, time, includeTime) = _dateDataFromCellData(cellData);
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
_dateDataFromCellData(cellData);
return DateCellCalendarState(
dateTypeOptionPB: dateTypeOptionPB,
format: CalendarFormat.month,
focusedDay: DateTime.now(),
startDay: isRange ? dateTime : null,
endDay: isRange ? endDateTime : null,
dateTime: dateTime,
endDateTime: endDateTime,
time: time,
endTime: endTime,
includeTime: includeTime,
timeFormatError: null,
isRange: isRange,
parseTimeError: null,
parseEndTimeError: null,
timeHintText: _timeHintText(dateTypeOptionPB),
);
}
@@ -300,21 +379,31 @@ String _timeHintText(DateTypeOptionPB typeOption) {
}
}
(DateTime?, String?, bool) _dateDataFromCellData(DateCellDataPB? cellData) {
(DateTime?, DateTime?, String?, String?, bool, bool) _dateDataFromCellData(
DateCellDataPB? cellData,
) {
// a null DateCellDataPB may be returned, indicating that all the fields are
// at their default values: empty strings and false booleans
// their default values: empty strings and false booleans
if (cellData == null) {
return (null, null, false);
return (null, null, null, null, false, false);
}
DateTime? dateTime;
String? time;
DateTime? endDateTime;
String? endTime;
if (cellData.hasTimestamp()) {
final timestamp = cellData.timestamp * 1000;
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
time = cellData.time;
if (cellData.hasEndTimestamp()) {
final endTimestamp = cellData.endTimestamp * 1000;
endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt());
endTime = cellData.endTime;
}
}
final bool includeTime = cellData.includeTime;
final bool isRange = cellData.isRange;
return (dateTime, time, includeTime);
return (dateTime, endDateTime, time, endTime, includeTime, isRange);
}
@@ -74,7 +74,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
controller: _popover,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(260, 520)),
constraints: BoxConstraints.loose(const Size(260, 620)),
margin: EdgeInsets.zero,
child: GridDateCellText(
dateStr: state.dateStr,
Loaded 100 of 514 files, more files were not shown because too many files have changed in this diff. Show more