Compare commits

...
106 Commits
Author SHA1 Message Date
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
Lucas.Xu daf9b48c73 chore: update CHANGELOG (#3433) 2023-09-18 14:39:25 +08:00
Lucas.Xu 0ea5b3c483 fix: rtl related issues 2023-09-18 14:33:51 +08:00
Nathan.fooo f3148640eb chore: update collab rev id (#3432) 2023-09-18 12:45:16 +08:00
Zackandnathan 1c84ee1d53 feat: integrate appflowy-cloud (#3359)
* feat: draft: code dependency

* chore: update ref

* feat: signup using client_api

* feat: support auto sign_in after sign_up if already confirmed(WIP)

* chore: update collab commit id

* chore: fix compile errors

* chore: user AFServer trait to provide optional service

* chore: refactor workspace

* chore: disable aws config

* chore: return ws connect

* chore: update collab rev

* chore: fmt and clippy

* chore: fix test

* chore: update chrono version

* chore: add script to update the collab crates commit id

* chore: update

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-09-17 17:14:34 +08:00
xfarrow cecd4f48ab Italian translation fix (#3395)
* Update it-IT.json
2023-09-15 15:43:05 +02:00
Richard Shiue 1e001640aa fix: cannot open popovers (#3410) 2023-09-15 19:01:22 +08:00
Lucas.Xu 3426e5b53e fix: launch review issues (#3413) 2023-09-15 17:51:29 +08:00
Yijing Huang 936b49a63f fix: alignment issue in login page (#3399) 2023-09-15 16:07:23 +08:00
Richard Shiue f6f80b48c9 fix: some UI issues (#3403) 2023-09-14 19:30:33 +08:00
Lucas.Xu 759cb27a43 chore: update version to 0.3.2 (#3404) 2023-09-14 19:23:39 +08:00
Lucas.Xu 26a2bffcd1 fix: some UI issues were present in version 0.3.1. (#3401) 2023-09-14 19:22:32 +08:00
Aman Negi fe55452c79 fix: restore banner theme issues (#3299) 2023-09-14 18:48:42 +08:00
Generic-Ripe 16adacac38 chore: update sv.json (#3386)
In this context board is tavla in Swedish.
Styrelse is board of directors.
2023-09-14 18:47:55 +08:00
Vincenzo De Petris d86531ac92 fix: add left padding to TabsManager widget (#3396) 2023-09-14 12:08:44 +02:00
Nathan.fooo 9231455f0d chore: update collab rev and fix compile (#3398) 2023-09-13 22:11:51 +08:00
Richard Shiue 0c6a1d4ae7 chore: revamp checklist ui (#3380)
* chore: revamp checklist editor  ui

* chore: checklist progress bar

* test: integration tests

* fix: flutter analyzer errors

* fix: checklist percentage complete
2023-09-13 20:44:04 +08:00
Richard Shiue 524efc2620 test: fi convert text to date cell test (#3377) 2023-09-13 19:12:40 +08:00
Richard Shiue 50a4f03931 chore: date editor ui polish (#3367) 2023-09-13 19:12:26 +08:00
Richard Shiue a4681a4042 fix: don't use timestamp fields to make calendar (#3366) 2023-09-13 19:11:46 +08:00
Richard Shiue 1ca130d7de feat: revamp row detail page UI (#3328)
* feat: revamp row detail page UI

* chore: some minor details and fix tests

* fix: fix tests

* chore: remove unused field

* chore: code cleanup

* test: add reordering fields in row page tests

* chore: remove duplicate and delete row events

* chore: timestamp cell ui adjustment

* chore: remove unused code

* test: fix new integration tests
2023-09-13 19:10:08 +08:00
Mathias Mogensen ef6f9a3175 fix: reorder rows on windows (#3279)
* fix: reorder rows on windows

Closes: #3208

* fix: unused import
2023-09-13 11:36:22 +08:00
Vincenzo De Petris aa25aa4474 fix: DateCardCellState date formatting (#3387)
* fix: DateCardCellState date formatting

* refactor: use string interpolation
2023-09-13 11:18:07 +08:00
c7af04b317 feat: support incremental updates for textblock's delta. (#3216)
* feat: support incremental to update textblock's delta

* fix: update test code

* fix: remove console

* fix: update test

* feat: integrate increamental delta in Flutter

* fix: delete quill editor

* fix: delete quill editor

* feat: add csharp in codeblock (#3371)

* chore: pt-PT & pt-BR translation updated  (#3353)

* chore: Ensure Cargo.lock Is Updated Alongside Changes to Cargo.toml (#3361)

* ci: add cargo check workflow

* ci: test cargo.toml

* fix: update test

* fix: code review

* fix: update cargo.toml and cargo.lock

* fix: code review

* fix: rust format

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Mayur Mahajan <47064215+MayurSMahajan@users.noreply.github.com>
Co-authored-by: Carlos Silva <mtbf99@gmail.com>
2023-09-12 20:49:03 +08:00
Mohammad ZolfaghariandLucas.Xu 9565173baf feat: text and layout direction settings (#3247)
* feat: text and layout direction settings

Added ltr|rtl|auto direction button to appflowy toolbar.
Introduced layout and default direction settings.

* chore: formate code

* feat: added hint for direction settings

* fix: flutter analyze

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-09-12 19:11:13 +08:00
Lucas.Xu 5f4e3ecc76 fix: unable to paste texts contains section or font tag (#3379) 2023-09-12 15:53:39 +08:00
Lucas.Xu 043c68a333 fix: downgrade the folder path to io.appflowy.appflowy (#3344) 2023-09-12 14:31:05 +08:00
Yijing Huang f639d51c11 feat: auth pages improvement for mobile and desktop platform (#3217) 2023-09-12 10:32:26 +08:00
Lucas.Xu 71071b60b2 feat: improve performance of document page (#3357) 2023-09-12 10:29:41 +08:00
Kilu.He 608255fa0f chore: Ensure Cargo.lock Is Updated Alongside Changes to Cargo.toml (#3361)
* ci: add cargo check workflow

* ci: test cargo.toml
2023-09-11 18:59:35 +08:00
Carlos Silva f8b0a26acc chore: pt-PT & pt-BR translation updated (#3353) 2023-09-11 15:24:08 +08:00
Mayur Mahajan 133dab934b feat: add csharp in codeblock (#3371) 2023-09-11 15:02:57 +08:00
fangwufeng-v a81670c447 feat: new grid (#3167)
* feat: implement database database service by functions

* feat: define database data type

* feat: basic grid store and component

* feat: data communication mechanism and simple table ui. Can add new filed and update field name.

* feat: add grid text cell and grid checkbox cell

* feat: single select cell and multiselect cell

* refactor: fix code review problems

* feat: add new row

* feat: fix tsc error
2023-09-11 10:27:56 +08:00
Richard Shiue 35f84e42cc fix: one-frame glitch when deleting all sorts (#3330) 2023-09-10 15:50:49 +08:00
Yijing Huang 2f6df6a17d chore: remove default ImageFilter (#3349) 2023-09-08 10:09:16 +08:00
Richard Shiue 8bcc6384f2 refactor: different TypeOption between DateTime and LastModified/CreatedAt (#3356)
* fix: stringify date cell includes time if true

* refactor: LastModified and CreatedAt type option

* chore: frontend implementation

* chore: some adjustments and fix tests

* fix: integration tests

* chore: timestamp type option ui
2023-09-08 10:07:24 +08:00
Nathan.fooo a0fc4b86b0 chore: update collab libs rev (#3342) 2023-09-06 16:00:23 +08:00
Nathan.fooo 790580eea4 chore: update cargo build profile (#3332) 2023-09-05 09:31:23 +08:00
Lucas.Xu 3db9024a2d chore: release 0.3.1 (#3326)
* chore: update editor version

* fix: export name (with CJK) doesn't match the document name

* chore: bump version 0.3.1
2023-09-04 17:28:19 +08:00
Nathan.fooo a821740469 refactor: remove http server ref (#3327) 2023-09-04 15:50:38 +08:00
Lucas.Xu af0ae2e375 feat: enable heading in the table plugin (#3325) 2023-09-04 13:35:11 +08:00
Nils Jacobsen 7d6fa0b159 chore: update inlang.config.js and language files 2023-09-04 13:33:56 +08:00
Richard Shiueandnathan b73a34ed94 refactor: remove date cell persistence (#3095)
* refactor: remove date cell persistence

* refactor: use i64 rather than String in DateChangeset

* chore: code cleanup

* fix: tauri build

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-09-02 11:50:16 +08:00
Richard Shiue f3aaff77b9 chore: per-view field visibility UI (#3296)
* chore: default field settings if not exist

* chore: field settings listeners and services

* chore: don't need to updateFieldInfos

* feat: per-view field visibilty UI

* fix: remove unresolved imports
2023-09-02 00:42:46 +08:00
Vedant PandeyandRichard Shiue 9b7ff375b2 fix: trigger option selection on tap down (#2915)
* fix: trigger option selection on tap down

* chore: add TODO comment

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2023-09-01 22:58:21 +08:00
Richard Shiueandnathan 18498c0479 feat: field settings application domain (#3284)
* chore: default field settings if not exist

* chore: field settings listeners and services

* chore: don't need to updateFieldInfos

* chore: load default field settings if none found

* chore: update collab ref

* chore: fix remidner compile errors

* fix: fix tests

* chore: update tauri project setting

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-09-01 22:40:17 +08:00
Nathan.fooo c652c32575 feat File storage (#3306)
* refactor: file upload

* refactor: support upload plan

* test: add tests
2023-09-01 22:27:29 +08:00
Mohammad Zolfaghari df8642d446 feat: support table plugin (#3280) 2023-09-01 14:45:21 +08:00
Nathan.fooo abb6eff23d fix: disable sync if keys aren't provided (#3304) 2023-09-01 11:32:45 +08:00
Nathan.fooo 3b4f8e53a2 feat: storage file (#3303) 2023-08-31 16:40:40 +08:00
Nathan.fooo 4847b8b114 chore: enable discord and github login (#3293) 2023-08-31 15:45:01 +08:00
Lucas.Xu e8f124b97c feat: support editing name when creating page (#3297) 2023-08-30 22:16:12 +08:00
Lucas.Xu f73d59fb57 feat: support text align and block align (#3292)
* feat: support text align and block align

* test: add test
2023-08-30 17:21:32 +08:00
Nathan.fooo 897d7980f6 chore: remove jwt secret env variable (#3291) 2023-08-30 11:51:33 +08:00
Mayur MahajanandLucas.Xu b88aed887a fix: paste multiple lines in codeblock (#3151)
* fix: paste multiple lines in codeblock

* fix: works with non collapsed selection

* chore: localize code block

* test: multiline paste in codeblock

* chore: remove unused import

* fix: only hanlde code block paste command if all the selected nodes are code block

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-08-30 09:58:56 +08:00
Alex Wallen 4e863bc87f feat: add hover color to question bubble like mockup (#3222) 2023-08-29 21:03:58 +02:00
nathan 507554a596 chore: only show email if supabase is enabled 2023-08-29 09:52:41 +08:00
Nathan.fooo 0806436c19 chore: share database via csv (#3285)
* chore: share database via csv

* fix: flutter test
2023-08-28 23:20:56 +08:00
Nathan.fooo 1205f0ebf7 chore: enable edit user email (#3286) 2023-08-28 21:00:55 +08:00
Aryan Arora 26e56194ea fix: nightmode support for no recents text on the emoji menu (#3239) 2023-08-28 17:09:20 +08:00
Nathan.fooo 4e67282f2b feat: Data sync on signup (#3283)
* chore: sync all data

* chore: sync database row and document in the row

* chore: sync inline view id

* chore: sync row and document in row

* fix: tests

* fix: migrate document in row

* chore: retry when create collab fail

* fix: invalid secret cause rerun application

* chore: fix clippy warnnings
2023-08-28 13:28:24 +08:00
Lucas.Xu 41ec2d992e feat: improve CJK IME support (#3278) 2023-08-28 10:58:26 +08:00
Richard Shiue f0e4f3db61 chore: per-view field settings (#3199)
* chore: field-setting entities-events-notifications

* chore: update field settings

* chore: add tests

* chore: add docs

* chore: use an enum for field visibility

* chore: clippy warnings

* fix: deps fields

* chore: collab merge main

* chore: collab ref

* test: fix tests

* fix: tauri bump collab rev
2023-08-27 22:31:32 +08:00
Lucas.Xu 6634a0ecb3 chore: update supabase version and setup new deeplink for linux (#3272) 2023-08-25 18:06:23 +08:00
Lucas.Xu 255f30590f feat: add custom context menu items for copy, cut, and paste commands 2023-08-24 22:23:50 +08:00
Nathan.fooo 1ba7224088 docs: improve documentation (#3265) 2023-08-24 14:00:34 +08:00
674 changed files with 32085 additions and 15759 deletions

No files matched your search

+38 -14
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:
@@ -385,6 +389,7 @@ jobs:
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 -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:
+30 -9
View File
@@ -32,6 +32,26 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Rust Dependencies
uses: Swatinem/rust-cache@v2
with:
key: rust-dependencies-${{ runner.os }}
workspaces: |
frontend/rust-lib
frontend/appflowy_tauri/src-tauri
- name: Cache Node.js dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: npm-${{ runner.os }}
- name: Cache node_modules
uses: actions/cache@v2
with:
path: frontend/appflowy_tauri/node_modules
key: node-modules-${{ runner.os }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
@@ -40,13 +60,6 @@ jobs:
override: true
profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: 'ubuntu-latest-tauri'
workspaces: |
frontend/rust-lib
frontend/appflowy_tauri/src-tauri
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
working-directory: frontend
@@ -75,15 +88,23 @@ jobs:
cargo make appflowy-tauri-deps-tools
npm install -g pnpm@${{ env.PNPM_VERSION }}
- name: build
- name: Build
working-directory: frontend/appflowy_tauri
run: |
mkdir dist
pnpm install
cargo make --cwd .. tauri_build
pnpm test
pnpm test:errors
- name: Check for uncommitted changes
run: |
diff_files=$(git status --porcelain)
if [ -n "$diff_files" ]; then
echo "There are uncommitted changes in the working tree. Please commit them before pushing."
exit 1
fi
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2
View File
@@ -41,3 +41,5 @@ pubspec.lock
# ignore the deb filegit
frontend/package
frontend/*.deb
**/Cargo.toml.bak
+47
View File
@@ -1,5 +1,52 @@
# Release Notes
## 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
- Improve the performance of the editor, now it is much faster when editing a large document.
- Support for reordering the rows of the database on Windows.
- Revamp the row detail page of the database.
- Revamp the checklist cell editor of the database.
### Bug fixes
- Some UI issues
## Version 0.3.1 - 09/04/2023
### New Features
- Improve CJK (Chinese, Japanese, Korean) input method support.
- Share a database in CSV format.
- Support for aligning the block component with the toolbar.
- Support for editing name when creating a new page.
- Support for inserting a table in the document page.
- Database views allow for independent field visibility toggling.
### Bug fixes
- Paste multiple lines in code block.
- Some UI issues
## Version 0.3.0 - 08/22/2023
### New Features
+9 -1
View File
@@ -20,11 +20,12 @@ run_task = { name = ["restore-crate-type"] }
[env]
RUST_LOG = "info"
CARGO_PROFILE = "dev"
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.0"
CURRENT_APP_VERSION = "0.3.3"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -75,6 +76,7 @@ BUILD_ACTIVE_ARCHS_ONLY = true
CRATE_TYPE = "staticlib"
[env.production-mac-arm64]
CARGO_PROFILE = "release"
BUILD_FLAG = "release"
TARGET_OS = "macos"
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
@@ -86,6 +88,7 @@ BUILD_ACTIVE_ARCHS_ONLY = false
CRATE_TYPE = "staticlib"
[env.production-mac-x86_64]
CARGO_PROFILE = "release"
BUILD_FLAG = "release"
TARGET_OS = "macos"
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
@@ -97,6 +100,7 @@ BUILD_ACTIVE_ARCHS_ONLY = false
CRATE_TYPE = "staticlib"
[env.production-mac-universal]
CARGO_PROFILE = "release"
BUILD_FLAG = "release"
TARGET_OS = "macos"
FLUTTER_OUTPUT_DIR = "Release"
@@ -114,6 +118,7 @@ CRATE_TYPE = "cdylib"
LIB_EXT = "dll"
[env.production-windows-x86]
CARGO_PROFILE = "release"
BUILD_FLAG = "release"
TARGET_OS = "windows"
RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
@@ -133,6 +138,7 @@ LIB_EXT = "so"
LINUX_ARCH = "x64"
[env.production-linux-x86_64]
CARGO_PROFILE = "release"
BUILD_FLAG = "release"
TARGET_OS = "linux"
RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
@@ -153,6 +159,7 @@ LINUX_ARCH = "arm64"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored"
[env.production-linux-aarch64]
CARGO_PROFILE = "release"
BUILD_FLAG = "release"
TARGET_OS = "linux"
RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
@@ -202,6 +209,7 @@ FLUTTER_OUTPUT_DIR = "Debug"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored"
[env.production-android]
CARGO_PROFILE = "release"
BUILD_FLAG = "release"
TARGET_OS = "android"
CRATE_TYPE = "cdylib"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

@@ -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,
);
});
@@ -9,7 +9,7 @@ import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('calendar database view', () {
group('calendar', () {
testWidgets('update calendar layout', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -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,9 +113,14 @@ 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
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDuplicateRowButton();
await tester.dismissRowDetailPage();
@@ -125,11 +130,23 @@ void main() {
// Delete an event
await tester.openCalendarEvent(index: 1);
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 {
@@ -143,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);
@@ -155,12 +172,12 @@ void main() {
// Delete the event
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
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);
@@ -179,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();
@@ -218,7 +218,6 @@ void main() {
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM dd, y').format(today),
);
@@ -233,7 +232,6 @@ void main() {
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM dd, y HH:mm').format(now),
);
@@ -247,7 +245,6 @@ void main() {
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y HH:mm').format(now),
);
@@ -261,7 +258,6 @@ void main() {
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y hh:mm a').format(now),
);
@@ -273,7 +269,6 @@ void main() {
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: '',
);
@@ -442,4 +437,106 @@ void main() {
await tester.pumpAndSettle();
});
});
testWidgets('edit checklist cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.Checklist;
await tester.createField(fieldType, fieldType.name);
// assert that there is no progress bar in the grid
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
// tap on the first checklist cell
await tester.tapChecklistCellInGrid(rowIndex: 0);
// assert that the checklist editor is shown
tester.assertChecklistEditorVisible(visible: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
// assert that the task is displayed
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 0",
isChecked: false,
);
// update the task's name
await tester.renameChecklistTask(index: 0, name: "task 1");
// assert that the task's name is updated
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
// dismiss new task editor
await tester.dismissCellEditor();
// dismiss checklist cell editor
await tester.dismissCellEditor();
// assert that progress bar is shown in grid at 0%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
// start editing the first checklist cell again
await tester.tapChecklistCellInGrid(rowIndex: 0);
// create another task with the create button
await tester.createNewChecklistTask(name: "task 2", button: true);
// assert that the task was inserted
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
// mark it as complete
await tester.checkChecklistTask(index: 1);
// assert that the task was checked in the editor
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: true,
);
// dismiss checklist editor
await tester.dismissCellEditor();
await tester.dismissCellEditor();
// assert that progressbar is shown in grid at 50%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
// re-open the cell editor
await tester.tapChecklistCellInGrid(rowIndex: 0);
// hover over first task and delete it
await tester.deleteChecklistTask(index: 0);
// dismiss cell editor
await tester.dismissCellEditor();
// assert that progressbar is shown in grid at 100%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
// re-open the cell edior
await tester.tapChecklistCellInGrid(rowIndex: 0);
// delete the remaining task
await tester.deleteChecklistTask(index: 0);
// dismiss the cell editor
await tester.dismissCellEditor();
// check that the progress bar is not viisble
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
});
}
@@ -0,0 +1,55 @@
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database field settings', () {
testWidgets('field visibility', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// hide the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapHidePropertyButton();
await tester.noFieldWithName('New field 1');
// go back to inline database view, expect field to be shown
await tester.tapTabBarLinkedViewByViewName('Untitled');
await tester.findFieldWithName('New field 1');
// go back to linked database view, expect field to be hidden
await tester.tapTabBarLinkedViewByViewName('grid');
await tester.noFieldWithName('New field 1');
// use the settings button to show the field
await tester.tapDatabaseSettingButton();
await tester.tapViewPropertiesButton();
await tester.tapViewTogglePropertyVisibilityButtonByName('New field 1');
await tester.dismissFieldEditor();
await tester.findFieldWithName('New field 1');
// open first row in popup then hide the field
await tester.openFirstRowDetailPage();
await tester.tapGridFieldWithNameInRowDetailPage('New field 1');
await tester.tapHidePropertyButtonInFieldEditor();
await tester.dismissRowDetailPage();
await tester.noFieldWithName('New field 1');
});
});
}
@@ -106,26 +106,6 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('hide field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// Delete the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapHidePropertyButton();
await tester.noFieldWithName('New field 1');
await tester.pumpAndSettle();
});
testWidgets('create checklist field ', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -135,7 +135,40 @@ void main() {
}
});
testWidgets('check document is exist in row detail page', (tester) async {
testWidgets('change order of fields and cells', (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 first field in the row details page is the select
// option tyoe
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
// Reorder first field in list
final gesture = await tester.hoverOnFieldInRowDetail(index: 0);
await tester.pumpAndSettle();
await tester.reorderFieldInRowDetail(offset: 30);
// Orders changed, now the checkbox is first
tester.assertFirstFieldInRowDetailByType(FieldType.Checkbox);
await gesture.removePointer();
await tester.pumpAndSettle();
// Reorder second field in list
await tester.hoverOnFieldInRowDetail(index: 1);
await tester.pumpAndSettle();
await tester.reorderFieldInRowDetail(offset: -30);
// First field is now back to select option
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
});
testWidgets('check document exists in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -149,7 +182,7 @@ void main() {
await tester.assertDocumentExistInRowDetailPage();
});
testWidgets('update the content of the document and re-open it',
testWidgets('update the contents of the document and re-open it',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -239,6 +272,7 @@ void main() {
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.tapEscButton();
@@ -255,6 +289,7 @@ void main() {
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDuplicateRowButton();
await tester.tapEscButton();
@@ -122,17 +122,17 @@ void main() {
}
// check the checklist cell
final List<double> checklistCells = [
0.6,
0.3,
final List<double?> checklistCells = [
0.67,
0.33,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
null,
null,
null,
null,
null,
null,
null,
];
for (final (index, percent) in checklistCells.indexed) {
await tester.assertChecklistCellInGrid(
@@ -157,7 +157,6 @@ void main() {
for (final (index, content) in dateCells.indexed) {
await tester.assertDateCellInGrid(
rowIndex: index,
fieldType: FieldType.DateTime,
content: content,
);
}
@@ -0,0 +1,46 @@
import 'package:appflowy/generated/flowy_svgs.g.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('document alignment', () {
testWidgets('edit alignment in toolbar', (tester) async {
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();
// click the align center
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
// expect to see the align center
final editorState = tester.editor.getCurrentEditorState();
final first = editorState.getNodeAtPath([0])!;
expect(first.attributes[blockComponentAlign], 'center');
// click the align right
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
expect(first.attributes[blockComponentAlign], 'right');
// click the align left
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
expect(first.attributes[blockComponentAlign], 'left');
});
});
}
@@ -0,0 +1,60 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('paste in codeblock', () {
testWidgets('paste multiple lines in codeblock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName();
// mock the clipboard
const lines = 3;
final text = List.generate(lines, (index) => 'line $index').join('\n');
AppFlowyClipboard.mockSetData(
AppFlowyClipboardData(
text: text,
),
);
await insertCodeBlockInDocument(tester);
// paste the text
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
expect(editorState.document.root.children.length, 1);
expect(
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
text,
);
});
});
}
/// Inserts an codeBlock in the document
Future<void> insertCodeBlockInDocument(WidgetTester tester) async {
// open the actions menu and insert the codeBlock
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_selectionMenu_codeBlock.tr(),
);
await tester.pumpAndSettle();
}
@@ -226,6 +226,56 @@ void main() {
);
},
);
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
testWidgets(
'paste the image from clipboard when html and image are both available',
(tester) async {
const html =
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
final image = await rootBundle.load('assets/test/images/sample.png');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(
html: html,
image: ('png', bytes),
(editorState) {
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(
node.attributes[ImageBlockKeys.url],
'https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png',
);
},
);
},
);
testWidgets('paste the html content contains section', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
await tester.pasteContent(
html: html,
(editorState) {
expect(editorState.document.root.children.length, 2);
final node1 = editorState.getNodeAtPath([0])!;
final node2 = editorState.getNodeAtPath([1])!;
expect(node1.type, ParagraphBlockKeys.type);
expect(node2.type, ParagraphBlockKeys.type);
},
);
});
testWidgets('paste the html from google translation', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
await tester.pasteContent(
html: html,
(editorState) {
expect(editorState.document.root.children.length, 8);
},
);
});
}
extension on WidgetTester {
@@ -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);
});
});
}
@@ -1,16 +1,20 @@
import 'package:integration_test/integration_test.dart';
import 'document_alignment_test.dart' as document_alignment_test;
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_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;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -25,4 +29,8 @@ void startTesting() {
document_with_outline_block.main();
document_with_toggle_list_test.main();
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();
}
@@ -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 {
@@ -4,6 +4,7 @@ import 'package:integration_test/integration_test.dart';
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_cell_test.dart' as database_cell_test;
import 'database_field_test.dart' as database_field_test;
import 'database_field_settings_test.dart' as database_field_settings_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_row_page_test.dart' as database_row_page_test;
import 'database_row_test.dart' as database_row_test;
@@ -47,6 +48,7 @@ void main() {
// Database integration tests
database_cell_test.main();
database_field_test.main();
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_row_test.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);
});
});
}
@@ -1,3 +1,4 @@
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';
@@ -7,7 +8,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -22,14 +23,11 @@ void main() {
await tester.tapGoButton();
// create a new page
const name = 'Hello AppFlowy';
await tester.tapNewPageButton();
await tester.enterText(find.byType(TextFormField), name);
await tester.tapOKButton();
// expect to see a new document
tester.expectToSeePageName(
name,
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
// and with one paragraph block
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
@@ -1,4 +1,4 @@
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -7,15 +7,15 @@ import 'base.dart';
extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
await tapButton(find.byType(GoogleSignUpButton));
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
}
Future<void> tapSignInAsGuest() async {
await tapButton(find.byType(SignInAsGuestButton));
await tapButton(find.byType(SignInAnonymousButton));
}
void expectToSeeGoogleLoginButton() {
expect(find.byType(GoogleSignUpButton), findsOneWidget);
expect(find.byKey(const Key('signInWithGoogleButton')), findsOneWidget);
}
void assertSwitchValue(Finder finder, bool value) {
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -9,8 +11,8 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
class FlowyTestContext {
FlowyTestContext({
@@ -39,8 +41,8 @@ extension AppFlowyTestBase on WidgetTester {
IntegrationMode.integrationTest,
);
await wait(3000);
await pumpAndSettle(const Duration(seconds: 2));
await waitUntilSignInPageShow();
return FlowyTestContext(
applicationDataDirectory: directory,
);
@@ -78,6 +80,27 @@ extension AppFlowyTestBase on WidgetTester {
return directory.path;
}
Future<void> waitUntilSignInPageShow() async {
final finder = find.byType(GoButton);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
}
Future<void> pumpUntilFound(
Finder finder, {
Duration timeout = const Duration(seconds: 10),
}) async {
bool timerDone = false;
final timer = Timer(timeout, () => timerDone = true);
while (timerDone != true) {
await pump();
if (any(finder)) {
timerDone = true;
}
}
timer.cancel();
}
Future<void> tapButton(
Finder finder, {
int? pointer,
@@ -1,14 +1,17 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
@@ -279,6 +282,14 @@ extension CommonOperations on WidgetTester {
// create a new page
await tapAddViewButton(name: parentName ?? gettingStarted);
await tapButtonWithName(layout.menuName);
final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
KVKeys.showRenameDialogWhenCreatingNewFile,
(value) => bool.parse(value),
);
final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
if (showRenameDialog) {
await tapOKButton();
}
await pumpAndSettle();
// hover on it and change it's name
@@ -409,6 +420,14 @@ extension CommonOperations on WidgetTester {
await gesture.up();
await pumpAndSettle();
}
// tap the button with [FlowySvgData]
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg.path == svg.path,
);
await tapButton(button);
}
}
extension ViewLayoutPBTest on ViewLayoutPB {
@@ -1,10 +1,12 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
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/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';
@@ -24,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';
@@ -35,8 +38,10 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
@@ -46,6 +51,7 @@ 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';
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';
@@ -240,36 +246,43 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
/// null percent means no progress bar should be found
Future<void> assertChecklistCellInGrid({
required int rowIndex,
required double percent,
required double? percent,
}) async {
final findCell = cellFinder(rowIndex, FieldType.Checklist);
final finder = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) {
if (widget is ChecklistProgressBar) {
return widget.percent == percent;
}
return false;
},
),
);
expect(finder, findsOneWidget);
if (percent == null) {
final finder = find.descendant(
of: findCell,
matching: find.byType(ChecklistProgressBar),
);
expect(finder, findsNothing);
} else {
final finder = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) {
if (widget is ChecklistProgressBar) {
return widget.percent == percent;
}
return false;
},
),
);
expect(finder, findsOneWidget);
}
}
Future<void> assertDateCellInGrid({
required int rowIndex,
required FieldType fieldType,
required String content,
}) async {
final findRow = find.byType(GridRow, skipOffstage: false);
final findCell = find.descendant(
of: findRow.at(rowIndex),
matching: find.byWidgetPredicate(
(widget) => widget is GridDateCell && widget.fieldType == fieldType,
),
matching: find.byType(GridDateCell),
skipOffstage: false,
);
@@ -318,7 +331,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,
@@ -450,6 +473,106 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(cell, matcher);
}
Future<void> tapChecklistCellInGrid({required int rowIndex}) async {
final findRow = find.byType(GridRow);
final findCell = finderForFieldType(FieldType.Checklist);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: findCell,
);
await tapButton(cell);
}
void assertChecklistEditorVisible({required bool visible}) {
final editor = find.byType(GridChecklistCellEditor);
if (visible) {
expect(editor, findsOneWidget);
} else {
expect(editor, findsNothing);
}
}
Future<void> createNewChecklistTask({
required String name,
enter = false,
button = false,
}) async {
assert(!(enter && button));
final textField = find.descendant(
of: find.byType(NewTaskItem),
matching: find.byType(TextField),
);
await enterText(textField, name);
await pumpAndSettle();
if (enter) {
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
} else {
await tapButton(
find.descendant(
of: find.byType(NewTaskItem),
matching: find.byType(FlowyTextButton),
),
);
}
}
void assertChecklistTaskInEditor({
required int index,
required String name,
required bool isChecked,
}) {
final task = find.byType(ChecklistItem).at(index);
final widget = this.widget<ChecklistItem>(task);
assert(
widget.option.data.name == name && widget.option.isSelected == isChecked,
);
}
Future<void> renameChecklistTask({
required int index,
required String name,
}) async {
final textField = find
.descendant(
of: find.byType(ChecklistItem),
matching: find.byType(TextField),
)
.at(index);
await enterText(textField, name);
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
}
Future<void> checkChecklistTask({required int index}) async {
final button = find.descendant(
of: find.byType(ChecklistItem).at(index),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,
),
);
await tapButton(button);
}
Future<void> deleteChecklistTask({required int index}) async {
final task = find.byType(ChecklistItem).at(index);
await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
await pumpAndSettle();
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
);
await tapButton(button);
}
Future<void> openFirstRowDetailPage() async {
await hoverOnFirstRowOfGrid();
@@ -486,7 +609,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(banner, findsOneWidget);
await startGesture(
getTopLeft(banner),
getCenter(banner),
kind: PointerDeviceKind.mouse,
);
@@ -503,6 +626,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(findDateCell);
}
Future<void> tapGridFieldWithNameInRowDetailPage(String name) async {
final fields = find.byWidgetPredicate(
(widget) => widget is FieldCellButton && widget.field.name == name,
);
final field = find.descendant(
of: find.byType(RowDetailPage),
matching: fields,
);
await tapButton(field);
await pumpAndSettle();
}
Future<void> duplicateRowInRowDetailPage() async {
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
await tapButton(duplicateButton);
@@ -513,6 +648,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(deleteButton);
}
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
final fieldButtons = find.byType(FieldCellButton);
final button = find
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
.at(index);
return startGesture(
getCenter(button),
kind: PointerDeviceKind.mouse,
);
}
Future<void> reorderFieldInRowDetail({required double offset}) async {
final thumb = find
.byWidgetPredicate(
(widget) => widget is ReorderableDragStartListener && widget.enabled,
)
.first;
await drag(
thumb,
Offset(0, offset),
kind: PointerDeviceKind.mouse,
);
await pumpAndSettle();
}
Future<void> scrollGridByOffset(Offset offset) async {
await drag(find.byType(GridPage), offset);
await pumpAndSettle();
@@ -585,6 +745,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(field);
}
Future<void> tapHidePropertyButtonInFieldEditor() async {
final button = find.byType(HideFieldButton);
await tapButton(button);
}
Future<void> tapRowDetailPageRowActionButton() async {
await tapButton(find.byType(RowActionButton));
}
Future<void> tapRowDetailPageCreatePropertyButton() async {
await tapButton(find.byType(CreateRowFieldButton));
}
@@ -654,6 +823,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(field, findsOneWidget);
}
void assertFirstFieldInRowDetailByType(FieldType fieldType) {
final firstField = find
.descendant(
of: find.byType(RowDetailPage),
matching: find.byType(FieldCellButton),
)
.first;
final widget = this.widget<FieldCellButton>(firstField);
expect(widget.field.fieldType, fieldType);
}
Future<void> findFieldWithName(String name) async {
final field = find.byWidgetPredicate(
(widget) => widget is FieldCellButton && widget.field.name == name,
@@ -718,9 +899,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);
}
@@ -925,6 +1104,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
/// Should call [tapDatabaseSettingButton] first.
Future<void> tapViewPropertiesButton() async {
final findSettingItem = find.byType(DatabaseSettingItem);
final findLayoutButton = find.byWidgetPredicate(
(widget) =>
widget is FlowyText &&
widget.text == DatabaseSettingAction.showProperties.title(),
);
final button = find.descendant(
of: findSettingItem,
matching: findLayoutButton,
);
await tapButton(button);
}
/// Should call [tapDatabaseSettingButton] first.
Future<void> tapDatabaseLayoutButton() async {
final findSettingItem = find.byType(DatabaseSettingItem);
@@ -1094,12 +1290,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);
@@ -1111,6 +1367,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(findCreateButton);
}
Future<void> tapTabBarLinkedViewByViewName(String name) async {
final viewButton = findTabBarLinkViewByViewName(name);
await tapButton(viewButton);
}
Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
return find.byWidgetPredicate(
(widget) => widget is TabBarItemButton && widget.view.layout == layout,
@@ -1212,6 +1473,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> tapAddSelectOptionButton() async {
await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());
}
Future<void> tapViewTogglePropertyVisibilityButtonByName(
String fieldName,
) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is GridPropertyCell && widget.fieldInfo.name == fieldName,
);
final toggleVisibilityButton =
find.descendant(of: field, matching: find.byType(FlowyIconButton));
await tapButton(toggleVisibilityButton);
}
}
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
@@ -1235,7 +1508,7 @@ Finder finderForFieldType(FieldType fieldType) {
return find.byType(GridDateCell, skipOffstage: false);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return find.byType(GridDateCell, skipOffstage: false);
return find.byType(GridTimestampCell, skipOffstage: false);
case FieldType.SingleSelect:
return find.byType(GridSingleSelectCell, skipOffstage: false);
case FieldType.MultiSelect:
@@ -1,6 +1,7 @@
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';
@@ -174,4 +175,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
@@ -50,6 +50,8 @@ PODS:
- Toast
- integration_test (0.0.1):
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@@ -66,6 +68,8 @@ PODS:
- FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- super_native_extensions (0.0.1):
- Flutter
- SwiftyGif (5.4.3)
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
@@ -83,11 +87,13 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
@@ -119,6 +125,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context:
:path: ".symlinks/plugins/irondash_engine_context/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@@ -129,6 +137,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
@@ -146,6 +156,7 @@ SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
@@ -153,6 +164,7 @@ SPEC CHECKSUMS:
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
super_native_extensions: 49e897b6039bb784226e7354e502a3c68e1659b5
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
@@ -16,6 +16,17 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
@@ -7,6 +7,10 @@ import 'package:shared_preferences/shared_preferences.dart';
abstract class KeyValueStorage {
Future<void> set(String key, String value);
Future<Either<FlowyError, String>> get(String key);
Future<Either<FlowyError, T>> getWithFormat<T>(
String key,
T Function(String value) formatter,
);
Future<void> remove(String key);
Future<void> clear();
}
@@ -26,6 +30,18 @@ class DartKeyValue implements KeyValueStorage {
return Left(FlowyError());
}
@override
Future<Either<FlowyError, T>> getWithFormat<T>(
String key,
T Function(String value) formatter,
) async {
final value = await get(key);
return value.fold(
(l) => left(l),
(r) => right(formatter(r)),
);
}
@override
Future<void> remove(String key) async {
await _initSharedPreferencesIfNeeded();
@@ -71,6 +87,18 @@ class RustKeyValue implements KeyValueStorage {
return response.swap().map((r) => r.value);
}
@override
Future<Either<FlowyError, T>> getWithFormat<T>(
String key,
T Function(String value) formatter,
) async {
final value = await get(key);
return value.fold(
(l) => left(l),
(r) => right(formatter(r)),
);
}
@override
Future<void> remove(String key) async {
await ConfigEventRemoveKeyValue(
@@ -29,6 +29,8 @@ class KVKeys {
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily';
static const String kDocumentAppearanceDefaultTextDirection =
'kDocumentAppearanceDefaultTextDirection';
/// The key for saving the expanded views
///
@@ -41,4 +43,10 @@ class KVKeys {
/// The value is a json string with the following format:
/// {'SidebarFolderCategoryType.value': true}
static const String expandedFolders = 'expandedFolders';
/// The key for saving if showing the rename dialog when creating a new file
///
/// The value is a boolean string.
static const String showRenameDialogWhenCreatingNewFile =
'showRenameDialogWhenCreatingNewFile';
}
+1 -10
View File
@@ -29,21 +29,12 @@ abstract class Env {
defaultValue: '',
)
static final String supabaseAnonKey = _Env.supabaseAnonKey;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_JWT_SECRET',
defaultValue: '',
)
static final String supabaseJwtSecret = _Env.supabaseJwtSecret;
}
bool get isSupabaseEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return Env.supabaseUrl.isNotEmpty &&
Env.supabaseAnonKey.isNotEmpty &&
Env.supabaseJwtSecret.isNotEmpty;
return Env.supabaseUrl.isNotEmpty && Env.supabaseAnonKey.isNotEmpty;
} else {
return false;
}
@@ -0,0 +1,47 @@
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'),
)
],
),
),
);
}
}
@@ -2,6 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'cell_controller.dart';
@@ -13,7 +14,8 @@ typedef NumberCellController = CellController<String, String>;
typedef SelectOptionCellController
= CellController<SelectOptionCellDataPB, String>;
typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
typedef DateCellController = CellController<DateCellDataPB, DateCellData>;
typedef DateCellController = CellController<DateCellDataPB, String>;
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
typedef URLCellController = CellController<URLCellDataPB, String>;
class CellControllerBuilder {
@@ -41,20 +43,31 @@ class CellControllerBuilder {
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
parser: DateCellDataParser(),
reloadOnFieldChanged: true,
);
return DateCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
DateCellDataPersistence(cellContext: _cellContext),
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
parser: TimestampCellDataParser(),
reloadOnFieldChanged: true,
);
return TimestampCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.Number:
final cellDataLoader = CellDataLoader(
@@ -73,6 +73,16 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
}
}
class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
@override
TimestampCellDataPB? parserData(List<int> data) {
if (data.isEmpty) {
return null;
}
return TimestampCellDataPB.fromBuffer(data);
}
}
class SelectOptionCellDataParser
implements CellDataParser<SelectOptionCellDataPB> {
@override
@@ -1,7 +1,7 @@
part of 'cell_service.dart';
/// Save the cell data to disk
/// You can extend this class to do custom operations. For example, the DateCellDataPersistence.
/// You can extend this class to do custom operations.
abstract class CellDataPersistence<D> {
Future<Option<FlowyError>> save(D data);
}
@@ -28,51 +28,3 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
});
}
}
@freezed
class DateCellData with _$DateCellData {
const factory DateCellData({
DateTime? dateTime,
String? time,
required bool includeTime,
bool? clearFlag,
}) = _DateCellData;
}
class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
final DatabaseCellContext cellContext;
DateCellDataPersistence({
required this.cellContext,
});
@override
Future<Option<FlowyError>> save(DateCellData data) {
final payload = DateChangesetPB.create()
..cellId = _makeCellPath(cellContext);
if (data.dateTime != null) {
final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString();
payload.date = date;
}
if (data.time != null) {
payload.time = data.time!;
}
if (data.clearFlag != null) {
payload.clearFlag = data.clearFlag!;
}
payload.includeTime = data.includeTime;
return DatabaseEventUpdateDateCell(payload).send().then((result) {
return result.fold(
(l) => none(),
(err) => Some(err),
);
});
}
}
CellIdPB _makeCellPath(DatabaseCellContext cellId) {
return CellIdPB.create()
..viewId = cellId.viewId
..fieldId = cellId.fieldId
..rowId = cellId.rowId;
}
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
import 'package:appflowy_backend/protobuf/flowy-database2/date_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';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart';
class ChecklistCellBackendService {
final String viewId;
@@ -52,14 +53,19 @@ class ChecklistCellBackendService {
return DatabaseEventUpdateChecklistCell(payload).send();
}
Future<Either<Unit, FlowyError>> update({
Future<Either<Unit, FlowyError>> updateName({
required SelectOptionPB option,
required name,
}) {
option.freeze();
final newOption = option.rebuild((option) {
option.name = name;
});
final payload = ChecklistCellDataChangesetPB.create()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..updateOptions.add(option);
..updateOptions.add(newOption);
return DatabaseEventUpdateChecklistCell(payload).send();
}
@@ -0,0 +1,58 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:fixnum/fixnum.dart';
final class DateCellBackendService {
final CellIdPB cellId;
DateCellBackendService({
required String viewId,
required String fieldId,
required String rowId,
}) : cellId = CellIdPB.create()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
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
..isRange = isRange;
if (date != null) {
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
payload.date = Int64(dateTimestamp);
}
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();
}
Future<Either<Unit, FlowyError>> clear() {
final payload = DateChangesetPB.create()
..cellId = cellId
..clearFlag = true;
return DatabaseEventUpdateDateCell(payload).send();
}
}
@@ -395,13 +395,7 @@ class RowDataBuilder {
}
void insertDate(FieldInfo fieldInfo, DateTime date) {
assert(
[
FieldType.DateTime,
FieldType.LastEditedTime,
FieldType.CreatedTime,
].contains(fieldInfo.fieldType),
);
assert(FieldType.DateTime == fieldInfo.fieldType);
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
}
@@ -1,5 +1,7 @@
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'field_service.dart';
@@ -8,13 +10,18 @@ part 'field_action_sheet_bloc.freezed.dart';
class FieldActionSheetBloc
extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
final String fieldId;
final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService;
FieldActionSheetBloc({required FieldContext fieldCellContext})
: fieldService = FieldBackendService(
: fieldId = fieldCellContext.field.id,
fieldService = FieldBackendService(
viewId: fieldCellContext.viewId,
fieldId: fieldCellContext.field.id,
),
fieldSettingsService =
FieldSettingsBackendService(viewId: fieldCellContext.viewId),
super(
FieldActionSheetState.initial(
TypeOptionPB.create()..field_2 = fieldCellContext.field,
@@ -31,14 +38,20 @@ class FieldActionSheetBloc
);
},
hideField: (_HideField value) async {
final result = await fieldService.updateField(visibility: false);
final result = await fieldSettingsService.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysHidden,
);
result.fold(
(l) => null,
(err) => Log.error(err),
);
},
showField: (_ShowField value) async {
final result = await fieldService.updateField(visibility: true);
final result = await fieldSettingsService.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysShown,
);
result.fold(
(l) => null,
(err) => Log.error(err),
@@ -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';
@@ -29,8 +31,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
didReceiveFieldUpdate: (field) {
emit(state.copyWith(field: cellContext.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 +71,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 +83,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(),
resizeStart: 0,
);
}
@@ -1,24 +1,27 @@
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_listener.dart';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart';
import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/setting/setting_listener.dart';
import 'package:appflowy/plugins/database_view/application/sort/sort_listener.dart';
import 'package:appflowy/plugins/database_view/application/sort/sort_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:collection/collection.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/foundation.dart';
import '../../grid/presentation/widgets/filter/filter_info.dart';
import '../../grid/presentation/widgets/sort/sort_info.dart';
import '../database_view_service.dart';
import '../filter/filter_listener.dart';
import '../filter/filter_service.dart';
import '../row/row_cache.dart';
import '../setting/setting_listener.dart';
import '../setting/setting_service.dart';
import '../sort/sort_listener.dart';
import '../sort/sort_service.dart';
import 'field_info.dart';
import 'field_listener.dart';
@@ -72,20 +75,23 @@ typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
typedef OnReceiveFields = void Function(List<FieldInfo>);
typedef OnReceiveFilters = void Function(List<FilterInfo>);
typedef OnReceiveSorts = void Function(List<SortInfo>);
typedef OnReceiveFieldSettings = void Function(List<FieldInfo>);
class FieldController {
final String viewId;
// Listeners
final FieldsListener _fieldListener;
final DatabaseSettingListener _settingListener;
final FiltersListener _filtersListener;
final SortsListener _sortsListener;
final FieldSettingsListener _fieldSettingsListener;
// FFI services
final DatabaseViewBackendService _databaseViewBackendSvc;
final SettingBackendService _settingBackendSvc;
final FilterBackendService _filterBackendSvc;
final SortBackendService _sortBackendSvc;
final FieldSettingsBackendService _fieldSettingsBackendSvc;
bool _isDisposed = false;
@@ -97,18 +103,17 @@ class FieldController {
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
_updatedFieldCallbacks = {};
// Group callbacks
final Map<String, GroupSettingPB> _groupConfigurationByFieldId = {};
// Filter callbacks
final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};
_GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
final Map<String, FilterPB> _filterPBByFieldId = {};
// Sort callbacks
final Map<OnReceiveSorts, VoidCallback> _sortCallbacks = {};
_GridSortNotifier? _sortNotifier = _GridSortNotifier();
final Map<String, SortPB> _sortPBByFieldId = {};
// Database settings temporary storage
final Map<String, GroupSettingPB> _groupConfigurationByFieldId = {};
final List<FieldSettingsPB> _fieldSettings = [];
// Getters
List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];
@@ -116,39 +121,28 @@ class FieldController {
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
FieldInfo? getField(String fieldId) {
final fields = _fieldNotifier.fieldInfos
.where((element) => element.id == fieldId)
.toList();
if (fields.isEmpty) {
return null;
}
assert(fields.length == 1);
return fields.first;
return _fieldNotifier.fieldInfos
.firstWhereOrNull((element) => element.id == fieldId);
}
FilterInfo? getFilter(String filterId) {
final filters = _filterNotifier?.filters
.where((element) => element.filter.id == filterId)
.toList() ??
[];
if (filters.isEmpty) {
return null;
}
assert(filters.length == 1);
return filters.first;
FilterInfo? getFilterByFilterId(String filterId) {
return _filterNotifier?.filters
.firstWhereOrNull((element) => element.filterId == filterId);
}
SortInfo? getSort(String sortId) {
final sorts = _sortNotifier?.sorts
.where((element) => element.sortId == sortId)
.toList() ??
[];
if (sorts.isEmpty) {
return null;
}
assert(sorts.length == 1);
return sorts.first;
FilterInfo? getFilterByFieldId(String fieldId) {
return _filterNotifier?.filters
.firstWhereOrNull((element) => element.fieldId == fieldId);
}
SortInfo? getSortBySortId(String sortId) {
return _sortNotifier?.sorts
.firstWhereOrNull((element) => element.sortId == sortId);
}
SortInfo? getSortByFieldId(String fieldId) {
return _sortNotifier?.sorts
.firstWhereOrNull((element) => element.fieldId == fieldId);
}
FieldController({required this.viewId})
@@ -159,34 +153,18 @@ class FieldController {
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId),
_sortBackendSvc = SortBackendService(viewId: viewId),
_sortsListener = SortsListener(viewId: viewId),
_settingBackendSvc = SettingBackendService(viewId: viewId) {
//Listen on field's changes
_fieldSettingsListener = FieldSettingsListener(viewId: viewId),
_fieldSettingsBackendSvc = FieldSettingsBackendService(viewId: viewId) {
// Start listeners
_listenOnFieldChanges();
//Listen on setting changes
_listenOnSettingChanges();
//Listen on the filter changes
_listenOnFilterChanges();
//Listen on the sort changes
_listenOnSortChanged();
_settingBackendSvc.getSetting().then((result) {
if (_isDisposed) {
return;
}
result.fold(
(setting) => _updateSetting(setting),
(err) => Log.error(err),
);
});
_listenOnFieldSettingsChanged();
}
/// Listen for filter changes in the backend.
void _listenOnFilterChanges() {
//Listen on the filter changes
deleteFilterFromChangeset(
List<FilterInfo> filters,
FilterChangesetNotificationPB changeset,
@@ -196,9 +174,6 @@ class FieldController {
filters.retainWhere(
(element) => !deleteFilterIds.contains(element.filter.id),
);
_filterPBByFieldId
.removeWhere((key, value) => deleteFilterIds.contains(value.id));
}
}
@@ -216,7 +191,6 @@ class FieldController {
fieldType: newFilter.fieldType,
);
if (fieldInfo != null) {
_filterPBByFieldId[fieldInfo.id] = newFilter;
filters.add(FilterInfo(viewId, newFilter, fieldInfo));
}
}
@@ -234,8 +208,6 @@ class FieldController {
// Remove the old filter
if (filterIndex != -1) {
filters.removeAt(filterIndex);
_filterPBByFieldId
.removeWhere((key, value) => value.id == updatedFilter.filterId);
}
// Insert the filter if there is a filter and its field info is
@@ -257,7 +229,6 @@ class FieldController {
} else {
filters.add(filterInfo);
}
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
}
}
}
@@ -272,16 +243,17 @@ class FieldController {
result.fold(
(FilterChangesetNotificationPB changeset) {
final List<FilterInfo> filters = filterInfos;
// Deletes the filters
// delete removed filters
deleteFilterFromChangeset(filters, changeset);
// Inserts the new filter if it's not exist
// insert new filters
insertFilterFromChangeset(filters, changeset);
// edit modified filters
updateFilterFromChangeset(filters, changeset);
_updateFieldInfos();
_filterNotifier?.filters = filters;
_updateFieldInfos();
},
(err) => Log.error(err),
);
@@ -289,6 +261,7 @@ class FieldController {
);
}
/// Listen for sort changes in the backend.
void _listenOnSortChanged() {
deleteSortFromChangeset(
List<SortInfo> newSortInfos,
@@ -299,9 +272,6 @@ class FieldController {
newSortInfos.retainWhere(
(element) => !deleteSortIds.contains(element.sortId),
);
_sortPBByFieldId
.removeWhere((key, value) => deleteSortIds.contains(value.id));
}
}
@@ -320,7 +290,6 @@ class FieldController {
);
if (fieldInfo != null) {
_sortPBByFieldId[newSortPB.fieldId] = newSortPB;
newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo));
}
}
@@ -356,7 +325,6 @@ class FieldController {
} else {
newSortInfos.add(newSortInfo);
}
_sortPBByFieldId[updatedSort.fieldId] = updatedSort;
}
}
}
@@ -373,7 +341,6 @@ class FieldController {
insertSortFromChangeset(newSortInfos, changeset);
updateSortFromChangeset(newSortInfos, changeset);
_updateFieldInfos();
_sortNotifier?.sorts = newSortInfos;
},
(err) => Log.error(err),
@@ -382,8 +349,8 @@ class FieldController {
);
}
/// Listen for databse setting changes in the backend.
void _listenOnSettingChanges() {
//Listen on setting changes
_settingListener.start(
onSettingUpdated: (result) {
if (_isDisposed) {
@@ -398,19 +365,95 @@ class FieldController {
);
}
/// Listen for field changes in the backend.
void _listenOnFieldChanges() {
//Listen on field's changes
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;
},
);
if (fieldSettings == null) {
return fieldInfo;
}
final updatedFieldInfo =
fieldInfo.copyWith(fieldSettings: fieldSettings);
return updatedFieldInfo;
});
}
Future<void> insertFields(List<IndexFieldPB> insertedFields) async {
if (insertedFields.isEmpty) {
return;
}
final List<FieldInfo> newFieldInfos = fieldInfos;
for (final indexField in insertedFields) {
final initial = FieldInfo.initial(indexField.field_1);
final fieldInfo = await attachFieldSettings(initial);
if (newFieldInfos.length > indexField.index) {
newFieldInfos.insert(indexField.index, fieldInfo);
} else {
newFieldInfos.add(fieldInfo);
}
}
_fieldNotifier.fieldInfos = newFieldInfos;
}
Future<List<FieldInfo>> updateFields(List<FieldPB> updatedFieldPBs) async {
if (updatedFieldPBs.isEmpty) {
return [];
}
final List<FieldInfo> newFields = fieldInfos;
final List<FieldInfo> updatedFields = [];
for (final updatedFieldPB in updatedFieldPBs) {
final index =
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
if (index != -1) {
newFields.removeAt(index);
final initial = FieldInfo.initial(updatedFieldPB);
final fieldInfo = await attachFieldSettings(initial);
newFields.insert(index, fieldInfo);
updatedFields.add(fieldInfo);
}
}
if (updatedFields.isNotEmpty) {
_fieldNotifier.fieldInfos = newFields;
}
return updatedFields;
}
// Listen on field's changes
_fieldListener.start(
onFieldsChanged: (result) {
onFieldsChanged: (result) async {
result.fold(
(changeset) {
(changeset) async {
if (_isDisposed) {
return;
}
_deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields);
deleteFields(changeset.deletedFields);
insertFields(changeset.insertedFields);
final updatedFields = _updateFields(changeset.updatedFields);
final updatedFields = await updateFields(changeset.updatedFields);
for (final listener in _updatedFieldCallbacks.values) {
listener(updatedFields);
}
@@ -421,31 +464,70 @@ class FieldController {
);
}
/// Listen for field setting changes in the backend.
void _listenOnFieldSettingsChanged() {
FieldInfo updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
final List<FieldInfo> newFields = fieldInfos;
FieldInfo updatedField = newFields[0];
final index = newFields
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
if (index != -1) {
newFields[index] =
newFields[index].copyWith(fieldSettings: updatedFieldSettings);
updatedField = newFields[index];
}
_fieldNotifier.fieldInfos = newFields;
return updatedField;
}
_fieldSettingsListener.start(
onFieldSettingsChanged: (result) {
if (_isDisposed) {
return;
}
result.fold(
(fieldSettings) {
final updatedFieldInfo = updateFieldSettings(fieldSettings);
for (final listener in _updatedFieldCallbacks.values) {
listener([updatedFieldInfo]);
}
},
(err) => Log.error(err),
);
},
);
}
/// Updates sort, filter, group and field info from `DatabaseViewSettingPB`
void _updateSetting(DatabaseViewSettingPB setting) {
_groupConfigurationByFieldId.clear();
for (final configuration in setting.groupSettings.items) {
_groupConfigurationByFieldId[configuration.fieldId] = configuration;
}
for (final filter in setting.filters.items) {
_filterPBByFieldId[filter.fieldId] = filter;
}
_filterNotifier?.filters = _filterInfoListFromPBs(setting.filters.items);
for (final sort in setting.sorts.items) {
_sortPBByFieldId[sort.fieldId] = sort;
}
_sortNotifier?.sorts = _sortInfoListFromPBs(setting.sorts.items);
_fieldSettings.clear();
_fieldSettings.addAll(setting.fieldSettings.items);
_updateFieldInfos();
}
/// Attach sort, filter, group information and field settings to `FieldInfo`
void _updateFieldInfos() {
final List<FieldInfo> newFieldInfos = [];
for (final field in _fieldNotifier.fieldInfos) {
newFieldInfos.add(
field.copyWith(
fieldSettings: _fieldSettings
.firstWhereOrNull((setting) => setting.fieldId == field.id),
isGroupField: _groupConfigurationByFieldId[field.id] != null,
hasFilter: _filterPBByFieldId[field.id] != null,
hasSort: _sortPBByFieldId[field.id] != null,
hasFilter: getFilterByFieldId(field.id) != null,
hasSort: getSortByFieldId(field.id) != null,
),
);
}
@@ -453,52 +535,27 @@ class FieldController {
_fieldNotifier.fieldInfos = newFieldInfos;
}
Future<void> dispose() async {
if (_isDisposed) {
Log.warn('FieldController is already disposed');
return;
}
_isDisposed = true;
await _fieldListener.stop();
await _filtersListener.stop();
await _settingListener.stop();
await _sortsListener.stop();
for (final callback in _fieldCallbacks.values) {
_fieldNotifier.removeListener(callback);
}
_fieldNotifier.dispose();
for (final callback in _filterCallbacks.values) {
_filterNotifier?.removeListener(callback);
}
for (final callback in _sortCallbacks.values) {
_sortNotifier?.removeListener(callback);
}
_filterNotifier?.dispose();
_filterNotifier = null;
_sortNotifier?.dispose();
_sortNotifier = null;
}
/// Load all of the fields. This is required when opening the database
Future<Either<Unit, FlowyError>> loadFields({
required List<FieldIdPB> fieldIds,
}) async {
final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds);
return Future(
() => result.fold(
(newFields) {
(newFields) async {
if (_isDisposed) {
return left(unit);
}
_fieldNotifier.fieldInfos =
newFields.map((field) => FieldInfo.initial(field)).toList();
_loadFilters();
_loadSorts();
await Future.wait([
_loadFilters(),
_loadSorts(),
_loadAllFieldSettings(),
]);
_updateFieldInfos();
return left(unit);
},
(err) => right(err),
@@ -506,24 +563,12 @@ class FieldController {
);
}
/// Load all the filters from the backend. Required by `loadFields`
Future<Either<Unit, FlowyError>> _loadFilters() async {
return _filterBackendSvc.getAllFilters().then((result) {
return result.fold(
(filterPBs) {
final List<FilterInfo> filters = [];
for (final filterPB in filterPBs) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: filterPB.fieldId,
fieldType: filterPB.fieldType,
);
if (fieldInfo != null) {
final filterInfo = FilterInfo(viewId, filterPB, fieldInfo);
filters.add(filterInfo);
}
}
_filterNotifier?.filters = filters;
_filterNotifier?.filters = _filterInfoListFromPBs(filterPBs);
return left(unit);
},
(err) => right(err),
@@ -531,26 +576,12 @@ class FieldController {
});
}
/// Load all the sorts from the backend. Required by `loadFields`
Future<Either<Unit, FlowyError>> _loadSorts() async {
return _sortBackendSvc.getAllSorts().then((result) {
return result.fold(
(sortPBs) {
final List<SortInfo> sortInfos = [];
for (final sortPB in sortPBs) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: sortPB.fieldId,
fieldType: sortPB.fieldType,
);
if (fieldInfo != null) {
final sortInfo = SortInfo(sortPB: sortPB, fieldInfo: fieldInfo);
sortInfos.add(sortInfo);
}
}
_updateFieldInfos();
_sortNotifier?.sorts = sortInfos;
_sortNotifier?.sorts = _sortInfoListFromPBs(sortPBs);
return left(unit);
},
(err) => right(err),
@@ -558,6 +589,56 @@ class FieldController {
});
}
/// Load all the field settings from the backend. Required by `loadFields`
Future<Either<Unit, FlowyError>> _loadAllFieldSettings() async {
return _fieldSettingsBackendSvc.getAllFieldSettings().then((result) {
return result.fold(
(fieldSettingsList) {
_fieldSettings.clear();
_fieldSettings.addAll(fieldSettingsList);
return left(unit);
},
(err) => right(err),
);
});
}
/// Attach corresponding `FieldInfo`s to the `FilterPB`s
List<FilterInfo> _filterInfoListFromPBs(List<FilterPB> filterPBs) {
FilterInfo? getFilterInfo(FilterPB filterPB) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: filterPB.fieldId,
fieldType: filterPB.fieldType,
);
return fieldInfo != null ? FilterInfo(viewId, filterPB, fieldInfo) : null;
}
return filterPBs
.map((filterPB) => getFilterInfo(filterPB))
.whereType<FilterInfo>()
.toList();
}
/// Attach corresponding `FieldInfo`s to the `SortPB`s
List<SortInfo> _sortInfoListFromPBs(List<SortPB> sortPBs) {
SortInfo? getSortInfo(SortPB sortPB) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: sortPB.fieldId,
fieldType: sortPB.fieldType,
);
return fieldInfo != null
? SortInfo(sortPB: sortPB, fieldInfo: fieldInfo)
: null;
}
return sortPBs
.map((sortPB) => getSortInfo(sortPB))
.whereType<SortInfo>()
.toList();
}
void addListener({
OnReceiveFields? onReceiveFields,
OnReceiveUpdateFields? onFieldsChanged,
@@ -640,57 +721,35 @@ class FieldController {
}
}
void _deleteFields(List<FieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
/// Stop listeners, dispose notifiers and clear listener callbacks
Future<void> dispose() async {
if (_isDisposed) {
Log.warn('FieldController is already disposed');
return;
}
final List<FieldInfo> newFields = fieldInfos;
final Map<String, FieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
_isDisposed = true;
await _fieldListener.stop();
await _filtersListener.stop();
await _settingListener.stop();
await _sortsListener.stop();
await _fieldSettingsListener.stop();
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier.fieldInfos = newFields;
}
for (final callback in _fieldCallbacks.values) {
_fieldNotifier.removeListener(callback);
}
_fieldNotifier.dispose();
void _insertFields(List<IndexFieldPB> insertedFields) {
if (insertedFields.isEmpty) {
return;
for (final callback in _filterCallbacks.values) {
_filterNotifier?.removeListener(callback);
}
final List<FieldInfo> newFieldInfos = fieldInfos;
for (final indexField in insertedFields) {
final fieldInfo = FieldInfo.initial(indexField.field_1);
if (newFieldInfos.length > indexField.index) {
newFieldInfos.insert(indexField.index, fieldInfo);
} else {
newFieldInfos.add(fieldInfo);
}
}
_fieldNotifier.fieldInfos = newFieldInfos;
}
_filterNotifier?.dispose();
_filterNotifier = null;
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
if (updatedFieldPBs.isEmpty) {
return [];
for (final callback in _sortCallbacks.values) {
_sortNotifier?.removeListener(callback);
}
final List<FieldInfo> newFields = fieldInfos;
final List<FieldInfo> updatedFields = [];
for (final updatedFieldPB in updatedFieldPBs) {
final index =
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
if (index != -1) {
newFields.removeAt(index);
final fieldInfo = FieldInfo.initial(updatedFieldPB);
newFields.insert(index, fieldInfo);
updatedFields.add(fieldInfo);
}
}
if (updatedFields.isNotEmpty) {
_fieldNotifier.fieldInfos = newFields;
}
return updatedFields;
_sortNotifier?.dispose();
_sortNotifier = null;
}
}
@@ -727,12 +786,7 @@ FieldInfo? _findFieldInfo({
required String fieldId,
required FieldType fieldType,
}) {
final fieldIndex = fieldInfos.indexWhere((element) {
return element.id == fieldId && element.fieldType == fieldType;
});
if (fieldIndex != -1) {
return fieldInfos[fieldIndex];
} else {
return null;
}
return fieldInfos.firstWhereOrNull(
(element) => element.id == fieldId && element.fieldType == fieldType,
);
}
@@ -1,4 +1,5 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'field_info.freezed.dart';
@@ -8,6 +9,7 @@ class FieldInfo with _$FieldInfo {
const factory FieldInfo({
required FieldPB field,
required FieldSettingsPB? fieldSettings,
required bool isGroupField,
required bool hasFilter,
required bool hasSort,
@@ -21,8 +23,11 @@ class FieldInfo with _$FieldInfo {
bool get isPrimary => field.isPrimary;
FieldVisibility? get visibility => fieldSettings?.visibility;
factory FieldInfo.initial(FieldPB field) => FieldInfo(
field: field,
fieldSettings: null,
hasFilter: false,
hasSort: false,
isGroupField: false,
@@ -30,7 +30,6 @@ class FieldBackendService {
Future<Either<Unit, FlowyError>> updateField({
String? name,
bool? frozen,
bool? visibility,
double? width,
}) {
final payload = FieldChangesetPB.create()
@@ -45,10 +44,6 @@ class FieldBackendService {
payload.frozen = frozen;
}
if (visibility != null) {
payload.visibility = visibility;
}
if (width != null) {
payload.width = width.toInt();
}
@@ -27,13 +27,6 @@ class DateTypeOptionBloc
),
);
},
includeTime: (_IncludeTime value) {
emit(
state.copyWith(
typeOption: _updateTypeOption(includeTime: value.includeTime),
),
);
},
);
},
);
@@ -42,7 +35,6 @@ class DateTypeOptionBloc
DateTypeOptionPB _updateTypeOption({
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
bool? includeTime,
}) {
state.typeOption.freeze();
return state.typeOption.rebuild((typeOption) {
@@ -63,8 +55,6 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
_DidSelectDateFormat;
const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
_DidSelectTimeFormat;
const factory DateTypeOptionEvent.includeTime(bool includeTime) =
_IncludeTime;
}
@freezed
@@ -0,0 +1,76 @@
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.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 'package:protobuf/protobuf.dart';
import 'type_option_context.dart';
part 'timestamp_bloc.freezed.dart';
class TimestampTypeOptionBloc
extends Bloc<TimestampTypeOptionEvent, TimestampTypeOptionState> {
TimestampTypeOptionBloc({
required TimestampTypeOptionContext typeOptionContext,
}) : super(TimestampTypeOptionState.initial(typeOptionContext.typeOption)) {
on<TimestampTypeOptionEvent>(
(event, emit) async {
event.map(
didSelectDateFormat: (_DidSelectDateFormat value) {
_updateTypeOption(dateFormat: value.format, emit: emit);
},
didSelectTimeFormat: (_DidSelectTimeFormat value) {
_updateTypeOption(timeFormat: value.format, emit: emit);
},
includeTime: (_IncludeTime value) {
_updateTypeOption(includeTime: value.includeTime, emit: emit);
},
);
},
);
}
void _updateTypeOption({
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
bool? includeTime,
required Emitter<TimestampTypeOptionState> emit,
}) {
state.typeOption.freeze();
final newTypeOption = state.typeOption.rebuild((typeOption) {
if (dateFormat != null) {
typeOption.dateFormat = dateFormat;
}
if (timeFormat != null) {
typeOption.timeFormat = timeFormat;
}
if (includeTime != null) {
typeOption.includeTime = includeTime;
}
});
emit(state.copyWith(typeOption: newTypeOption));
}
}
@freezed
class TimestampTypeOptionEvent with _$TimestampTypeOptionEvent {
const factory TimestampTypeOptionEvent.didSelectDateFormat(
DateFormatPB format,
) = _DidSelectDateFormat;
const factory TimestampTypeOptionEvent.didSelectTimeFormat(
TimeFormatPB format,
) = _DidSelectTimeFormat;
const factory TimestampTypeOptionEvent.includeTime(bool includeTime) =
_IncludeTime;
}
@freezed
class TimestampTypeOptionState with _$TimestampTypeOptionState {
const factory TimestampTypeOptionState({
required TimestampTypeOptionPB typeOption,
}) = _TimestampTypeOptionState;
factory TimestampTypeOptionState.initial(TimestampTypeOptionPB typeOption) =>
TimestampTypeOptionState(typeOption: typeOption);
}
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -58,7 +59,7 @@ class URLTypeOptionWidgetDataParser extends TypeOptionParser<URLTypeOptionPB> {
}
}
// Date
// DateTime
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
@@ -68,6 +69,17 @@ class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
}
}
// LastModified and CreatedAt
typedef TimestampTypeOptionContext = TypeOptionContext<TimestampTypeOptionPB>;
class TimestampTypeOptionDataParser
extends TypeOptionParser<TimestampTypeOptionPB> {
@override
TimestampTypeOptionPB fromBuffer(List<int> buffer) {
return TimestampTypeOptionPB.fromBuffer(buffer);
}
}
// SingleSelect
typedef SingleSelectTypeOptionContext
= TypeOptionContext<SingleSelectTypeOptionPB>;
@@ -0,0 +1,51 @@
import 'dart:typed_data';
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/notifier.dart';
typedef FieldSettingsValue = Either<FieldSettingsPB, FlowyError>;
class FieldSettingsListener {
final String viewId;
PublishNotifier<FieldSettingsValue>? _fieldSettingsNotifier =
PublishNotifier();
DatabaseNotificationListener? _listener;
FieldSettingsListener({required this.viewId});
void start({
required void Function(FieldSettingsValue) onFieldSettingsChanged,
}) {
_fieldSettingsNotifier?.addPublishListener(onFieldSettingsChanged);
_listener = DatabaseNotificationListener(
objectId: viewId,
handler: _handler,
);
}
void _handler(
DatabaseNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case DatabaseNotification.DidUpdateFieldSettings:
result.fold(
(payload) => _fieldSettingsNotifier?.value =
left(FieldSettingsPB.fromBuffer(payload)),
(error) => _fieldSettingsNotifier?.value = right(error),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_fieldSettingsNotifier?.dispose();
_fieldSettingsNotifier = null;
}
}
@@ -0,0 +1,54 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
class FieldSettingsBackendService {
final String viewId;
FieldSettingsBackendService({required this.viewId});
Future<Either<FieldSettingsPB, FlowyError>> getFieldSettings(
String fieldId,
) {
final id = FieldIdPB(fieldId: fieldId);
final ids = RepeatedFieldIdPB()..items.add(id);
final payload = FieldIdsPB()
..viewId = viewId
..fieldIds = ids;
return DatabaseEventGetFieldSettings(payload).send().then((result) {
return result.fold(
(fieldSettings) => left(fieldSettings.items.first),
(r) => right(r),
);
});
}
Future<Either<List<FieldSettingsPB>, FlowyError>> getAllFieldSettings() {
final payload = DatabaseViewIdPB()..value = viewId;
return DatabaseEventGetAllFieldSettings(payload).send().then((result) {
return result.fold(
(fieldSettings) => left(fieldSettings.items),
(r) => right(r),
);
});
}
Future<Either<Unit, FlowyError>> updateFieldSettings({
required String fieldId,
FieldVisibility? fieldVisibility,
}) {
final FieldSettingsChangesetPB payload = FieldSettingsChangesetPB.create()
..viewId = viewId
..fieldId = fieldId;
if (fieldVisibility != null) {
payload.visibility = fieldVisibility;
}
return DatabaseEventUpdateFieldSettings(payload).send();
}
}
@@ -32,11 +32,7 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
await _listenRowMeteChanged();
},
didReceiveRowMeta: (RowMetaPB rowMeta) {
emit(
state.copyWith(
rowMeta: rowMeta,
),
);
emit(state.copyWith(rowMeta: rowMeta));
},
setCover: (String coverURL) {
_updateMeta(coverURL: coverURL);
@@ -74,6 +74,7 @@ class RowCache {
final rowInfo = buildGridRow(row);
_rowList.add(rowInfo);
}
_changedNotifier.receive(const ChangedReason.setInitialRows());
}
Future<void> dispose() async {
@@ -245,7 +246,8 @@ class RowCache {
// ignore: prefer_collection_literals
final cellContextMap = CellContextByFieldId();
for (final fieldInfo in _fieldDelegate.fieldInfos) {
if (fieldInfo.field.visibility) {
if (fieldInfo.visibility != null &&
fieldInfo.visibility != FieldVisibility.AlwaysHidden) {
cellContextMap[fieldInfo.id] = DatabaseCellContext(
rowMeta: rowMeta,
viewId: viewId,
@@ -281,6 +283,7 @@ class RowChangesetNotifier extends ChangeNotifier {
initial: (_) {},
reorderRows: (_) => notifyListeners(),
reorderSingleRow: (_) => notifyListeners(),
setInitialRows: (_) => notifyListeners(),
);
}
}
@@ -313,6 +316,7 @@ class ChangedReason with _$ChangedReason {
ReorderSingleRowPB reorderRow,
RowInfo rowInfo,
) = _ReorderSingleRow;
const factory ChangedReason.setInitialRows() = _SetInitialRows;
}
class InsertedIndex {
@@ -1,6 +1,8 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@@ -28,13 +30,13 @@ class DatabasePropertyBloc
_startListening();
},
setFieldVisibility: (_SetFieldVisibility value) async {
final fieldBackendSvc = FieldBackendService(
final fieldSettingsSvc = FieldSettingsBackendService(
viewId: viewId,
fieldId: value.fieldId,
);
final result = await fieldBackendSvc.updateField(
visibility: value.visibility,
final result = await fieldSettingsSvc.updateFieldSettings(
fieldId: value.fieldId,
fieldVisibility: value.visibility,
);
result.fold((l) => null, (err) => Log.error(err));
@@ -84,7 +86,7 @@ class DatabasePropertyEvent with _$DatabasePropertyEvent {
const factory DatabasePropertyEvent.initial() = _Initial;
const factory DatabasePropertyEvent.setFieldVisibility(
String fieldId,
bool visibility,
FieldVisibility visibility,
) = _SetFieldVisibility;
const factory DatabasePropertyEvent.didReceiveFieldUpdate(
List<FieldInfo> fields,
@@ -0,0 +1,66 @@
import 'dart:io';
import 'package:appflowy/workspace/application/settings/share/export_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
part 'share_bloc.freezed.dart';
class DatabaseShareBloc extends Bloc<DatabaseShareEvent, DatabaseShareState> {
DatabaseShareBloc({
required this.view,
}) : super(const DatabaseShareState.initial()) {
on<ShareCSV>(_onShareCSV);
}
final ViewPB view;
Future<void> _onShareCSV(
ShareCSV event,
Emitter<DatabaseShareState> emit,
) async {
emit(const DatabaseShareState.loading());
final result = await BackendExportService.exportDatabaseAsCSV(view.id);
result.fold(
(l) => _saveCSVToPath(l.data, event.path),
(r) => Log.error(r),
);
emit(
DatabaseShareState.finish(
result.fold(
(l) {
_saveCSVToPath(l.data, event.path);
return left(unit);
},
(r) => right(r),
),
),
);
}
ExportDataPB _saveCSVToPath(String markdown, String path) {
File(path).writeAsStringSync(markdown);
return ExportDataPB()
..data = markdown
..exportType = ExportType.Markdown;
}
}
@freezed
class DatabaseShareEvent with _$DatabaseShareEvent {
const factory DatabaseShareEvent.shareCSV(String path) = ShareCSV;
}
@freezed
class DatabaseShareState with _$DatabaseShareState {
const factory DatabaseShareState.initial() = _Initial;
const factory DatabaseShareState.loading() = _Loading;
const factory DatabaseShareState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
@@ -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,82 @@
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();
if (!isClosed) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(
cells.values.toList(),
),
);
}
},
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) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(cells.values.toList()),
);
}
},
);
}
@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,
),
);
},
@@ -167,6 +237,29 @@ class EventCard extends StatelessWidget {
);
});
renderHook.addTimestampCellHook((cellData, cardData, _) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 3,
child: FlowyText.regular(
cellData.dateTime,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
});
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
if (selectedOptions.isEmpty) {
return const SizedBox.shrink();
@@ -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 SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
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,
),
);
}
}
@@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@@ -16,18 +17,25 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
GridHeaderBloc({
required this.viewId,
required this.fieldController,
}) : super(GridHeaderState.initial(fieldController.fieldInfos)) {
}) : super(GridHeaderState.initial()) {
on<GridHeaderEvent>(
(event, emit) async {
await event.map(
initial: (_InitialHeader value) async {
_startListening();
add(
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
);
},
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
emit(
state.copyWith(
fields: value.fields
.where((element) => element.field.visibility)
.where(
(element) =>
element.visibility != null &&
element.visibility != FieldVisibility.AlwaysHidden,
)
.toList(),
),
);
@@ -83,9 +91,5 @@ class GridHeaderState with _$GridHeaderState {
const factory GridHeaderState({required List<FieldInfo> fields}) =
_GridHeaderState;
factory GridHeaderState.initial(List<FieldInfo> fields) {
// final List<FieldPB> newFields = List.from(fields);
// newFields.retainWhere((field) => field.visibility);
return GridHeaderState(fields: fields);
}
factory GridHeaderState.initial() => const GridHeaderState(fields: []);
}
@@ -1,27 +1,26 @@
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_service.dart';
import '../../../application/field/field_service.dart';
import '../../../application/row/row_controller.dart';
part 'row_detail_bloc.freezed.dart';
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
final RowBackendService rowService;
final RowController dataController;
final RowController rowController;
RowDetailBloc({
required this.dataController,
}) : rowService = RowBackendService(viewId: dataController.viewId),
super(RowDetailState.initial()) {
required this.rowController,
}) : super(RowDetailState.initial()) {
on<RowDetailEvent>(
(event, emit) async {
await event.when(
initial: () async {
await _startListening();
final cells = dataController.loadData();
final cells = rowController.loadData();
if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
}
@@ -32,23 +31,32 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
deleteField: (fieldId) {
_fieldBackendService(fieldId).deleteField();
},
hideField: (fieldId) async {
final result = await _fieldBackendService(fieldId).updateField(
visibility: false,
showField: (fieldId) async {
final result =
await FieldSettingsBackendService(viewId: rowController.viewId)
.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysShown,
);
result.fold(
(l) {},
(err) => Log.error(err),
);
},
deleteRow: (rowId) async {
await rowService.deleteRow(rowId);
},
duplicateRow: (String rowId, String? groupId) async {
await rowService.duplicateRow(
rowId: rowId,
groupId: groupId,
hideField: (fieldId) async {
final result =
await FieldSettingsBackendService(viewId: rowController.viewId)
.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: FieldVisibility.AlwaysHidden,
);
result.fold(
(l) {},
(err) => Log.error(err),
);
},
reorderField: (fieldId, fromIndex, toIndex) async {
await _reorderField(fieldId, fromIndex, toIndex, emit);
},
);
},
@@ -57,12 +65,12 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
@override
Future<void> close() async {
dataController.dispose();
rowController.dispose();
return super.close();
}
Future<void> _startListening() async {
dataController.addListener(
rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
@@ -73,20 +81,42 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
FieldBackendService _fieldBackendService(String fieldId) {
return FieldBackendService(
viewId: dataController.viewId,
viewId: rowController.viewId,
fieldId: fieldId,
);
}
Future<void> _reorderField(
String fieldId,
int fromIndex,
int toIndex,
Emitter<RowDetailState> emit,
) async {
final cells = List<DatabaseCellContext>.from(state.cells);
cells.insert(toIndex, cells.removeAt(fromIndex));
emit(state.copyWith(cells: cells));
final fieldService =
FieldBackendService(viewId: rowController.viewId, fieldId: fieldId);
final result = await fieldService.moveField(
fromIndex,
toIndex,
);
result.fold((l) {}, (err) => Log.error(err));
}
}
@freezed
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.deleteRow(String rowId) = _DeleteRow;
const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
_DuplicateRow;
const factory RowDetailEvent.reorderField(
String fieldId,
int fromIndex,
int toIndex,
) = _ReorderField;
const factory RowDetailEvent.didReceiveCellDatas(
List<DatabaseCellContext> gridCells,
) = _DidReceiveCellDatas;
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
@@ -7,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';
@@ -257,59 +255,38 @@ class _GridRows extends StatelessWidget {
GridState state,
List<RowInfo> rowInfos,
) {
if (Platform.isWindows) {
// Workaround: On Windows, the focusing of the text cell is not working
// properly when the list is reorderable. So using the ListView instead.
return ListView.builder(
controller: scrollController.verticalController,
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: false,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
} else {
return ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
}
return ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
);
}
Widget _renderRow(
@@ -433,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,
),
),
],
),
),
);
},
@@ -448,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()} :';
}
@@ -114,7 +114,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
_FilterTextFieldDelegate();
double fixHeight = 46;
double fixHeight = 36;
@override
Widget build(
@@ -123,7 +123,7 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
bool overlapsContent,
) {
return Container(
padding: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.only(bottom: 4),
height: fixHeight,
child: FlowyTextField(
hintText: LocaleKeys.grid_settings_filterBy.tr(),
@@ -22,6 +22,10 @@ class FilterInfo {
);
}
String get filterId => filter.id;
String get fieldId => filter.fieldId;
DateFilterPB? dateFilter() {
if (![
FieldType.DateTime,
@@ -105,7 +105,6 @@ class _AddFilterButtonState extends State<AddFilterButton> {
return AppFlowyPopover(
controller: popoverController,
constraints: BoxConstraints.loose(const Size(200, 300)),
margin: const EdgeInsets.all(6),
triggerActions: PopoverTriggerFlags.none,
child: child,
popupBuilder: (BuildContext context) {
@@ -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
@@ -154,34 +159,33 @@ class FieldCellButton extends StatelessWidget {
final FieldPB field;
final int? maxLines;
final BorderRadius? radius;
final EdgeInsets? margin;
const FieldCellButton({
required this.field,
required this.onTap,
this.maxLines = 1,
this.radius = BorderRadius.zero,
this.margin,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Using this technique to have proper text ellipsis
// https://github.com/flutter/flutter/issues/18761#issuecomment-812390920
final text = Characters(field.name)
.replaceAll(Characters(''), Characters('\u{200B}'))
.toString();
return FlowyButton(
hoverColor: AFThemeExtension.of(context).greyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: onTap,
leftIcon: FlowySvg(
field.fieldType.icon(),
color: Theme.of(context).iconTheme.color,
),
radius: radius,
text: FlowyText.medium(
text,
field.name,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).textColor,
),
margin: GridSize.cellContentInsets,
margin: margin ?? GridSize.cellContentInsets,
);
}
}
@@ -52,8 +52,13 @@ class _FieldEditorState extends State<FieldEditor> {
@override
Widget build(BuildContext context) {
final bool requireSpace = widget.onDeleted != null ||
widget.onHidden != 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.typeOptionLoader.field.isPrimary)
@@ -102,7 +107,7 @@ class _FieldEditorState extends State<FieldEditor> {
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: _HideFieldButton(
child: HideFieldButton(
popoverMutex: popoverMutex,
onHidden: () {
state.field.fold(
@@ -200,6 +205,17 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
},
);
}
@override
void dispose() {
focusNode.removeListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex.close();
}
});
focusNode.dispose();
super.dispose();
}
}
class _DeleteFieldButton extends StatelessWidget {
@@ -236,11 +252,12 @@ class _DeleteFieldButton extends StatelessWidget {
}
}
class _HideFieldButton extends StatelessWidget {
@visibleForTesting
class HideFieldButton extends StatelessWidget {
final PopoverMutex popoverMutex;
final VoidCallback? onHidden;
const _HideFieldButton({
const HideFieldButton({
required this.popoverMutex,
required this.onHidden,
Key? key,
@@ -42,12 +42,10 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
final bloc = getIt<GridHeaderBloc>(
return getIt<GridHeaderBloc>(
param1: widget.viewId,
param2: widget.fieldController,
);
bloc.add(const GridHeaderEvent.initial());
return bloc;
)..add(const GridHeaderEvent.initial());
},
child: BlocBuilder<GridHeaderBloc, GridHeaderState>(
buildWhen: (previous, current) =>
@@ -97,7 +95,6 @@ class _GridHeaderState extends State<_GridHeader> {
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final cells = state.fields
.where((fieldInfo) => fieldInfo.field.visibility)
.map(
(field) => FieldContext(
viewId: widget.viewId,
@@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
@@ -20,6 +21,7 @@ import 'multi_select.dart';
import 'number.dart';
import 'rich_text.dart';
import 'single_select.dart';
import 'timestamp.dart';
import 'url.dart';
typedef TypeOptionData = Uint8List;
@@ -73,8 +75,6 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
),
);
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return DateTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<DateTypeOptionPB>(
viewId: viewId,
@@ -83,6 +83,16 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
),
popoverMutex,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<TimestampTypeOptionPB>(
viewId: viewId,
fieldType: fieldType,
dataController: dataController,
),
popoverMutex,
);
case FieldType.SingleSelect:
return SingleSelectTypeOptionWidgetBuilder(
makeTypeOptionContextWithDataController<SingleSelectTypeOptionPB>(
@@ -203,12 +213,16 @@ TypeOptionContext<T>
dataParser: CheckboxTypeOptionWidgetDataParser(),
) as TypeOptionContext<T>;
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return DateTypeOptionContext(
dataController: dataController,
dataParser: DateTypeOptionDataParser(),
) as TypeOptionContext<T>;
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampTypeOptionContext(
dataController: dataController,
dataParser: TimestampTypeOptionDataParser(),
) as TypeOptionContext<T>;
case FieldType.SingleSelect:
return SingleSelectTypeOptionContext(
dataController: dataController,
@@ -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();
}
}
@@ -0,0 +1,179 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'builder.dart';
import 'date.dart';
class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
final TimestampTypeOptionWidget _widget;
TimestampTypeOptionWidgetBuilder(
TimestampTypeOptionContext typeOptionContext,
PopoverMutex popoverMutex,
) : _widget = TimestampTypeOptionWidget(
typeOptionContext: typeOptionContext,
popoverMutex: popoverMutex,
);
@override
Widget? build(BuildContext context) {
return _widget;
}
}
class TimestampTypeOptionWidget extends TypeOptionWidget {
final TimestampTypeOptionContext typeOptionContext;
final PopoverMutex popoverMutex;
const TimestampTypeOptionWidget({
required this.typeOptionContext,
required this.popoverMutex,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
TimestampTypeOptionBloc(typeOptionContext: typeOptionContext),
child: BlocConsumer<TimestampTypeOptionBloc, TimestampTypeOptionState>(
listener: (context, state) =>
typeOptionContext.typeOption = state.typeOption,
builder: (context, state) {
final List<Widget> children = [
const TypeOptionSeparator(),
_renderDateFormatButton(context, state.typeOption.dateFormat),
_renderTimeFormatButton(context, state.typeOption.timeFormat),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: IncludeTimeButton(
onChanged: (value) => context
.read<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.includeTime(!value)),
value: state.typeOption.includeTime,
),
),
];
return ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
if (index == 0) {
return const SizedBox();
} else {
return VSpace(GridSize.typeOptionSeparatorHeight);
}
},
itemCount: children.length,
itemBuilder: (BuildContext context, int index) => children[index],
);
},
),
);
}
Widget _renderDateFormatButton(
BuildContext context,
DateFormatPB dataFormat,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) {
return DateFormatList(
selectedFormat: dataFormat,
onSelected: (format) {
context
.read<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.didSelectDateFormat(format));
PopoverContainer.of(popoverContext).close();
},
);
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(),
),
);
}
Widget _renderTimeFormatButton(
BuildContext context,
TimeFormatPB timeFormat,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) {
return TimeFormatList(
selectedFormat: timeFormat,
onSelected: (format) {
context
.read<TimestampTypeOptionBloc>()
.add(TimestampTypeOptionEvent.didSelectTimeFormat(format));
PopoverContainer.of(popoverContext).close();
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(timeFormat: timeFormat),
),
);
}
}
class IncludeTimeButton extends StatelessWidget {
final bool value;
final Function(bool value) onChanged;
const IncludeTimeButton({
super.key,
required this.onChanged,
required this.value,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: Row(
children: [
FlowySvg(
FlowySvgs.clock_alarm_s,
color: Theme.of(context).iconTheme.color,
),
const HSpace(6),
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
const Spacer(),
Toggle(
value: value,
onChanged: onChanged,
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
),
),
);
}
}
@@ -40,8 +40,7 @@ class RowActions extends StatelessWidget {
.map((action) => _ActionCell(action: action))
.toList();
//
final list = ListView.separated(
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
@@ -53,7 +52,6 @@ class RowActions extends StatelessWidget {
return cells[index];
},
);
return list;
},
),
);
@@ -70,6 +68,7 @@ class _ActionCell extends StatelessWidget {
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
useIntrinsicWidth: true,
text: FlowyText.medium(
action.title(),
color: action.enable()
@@ -161,16 +161,12 @@ class _RowLeadingState extends State<_RowLeading> {
index: widget.index!,
child: RowMenuButton(
isDragEnabled: isDraggable,
openMenu: () {
popoverController.show();
},
openMenu: popoverController.show,
),
),
] else ...[
RowMenuButton(
openMenu: () {
popoverController.show();
},
openMenu: popoverController.show,
),
],
],
@@ -261,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),
],
),
);
},
@@ -306,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 {
@@ -114,7 +114,7 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate {
_SortTextFieldDelegate();
double fixHeight = 46;
double fixHeight = 36;
@override
Widget build(
@@ -123,7 +123,7 @@ class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate {
bool overlapsContent,
) {
return Container(
padding: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.only(bottom: 4),
height: fixHeight,
child: FlowyTextField(
hintText: LocaleKeys.grid_settings_sortBy.tr(),
@@ -210,6 +210,7 @@ class DatabaseDeleteSortButton extends StatelessWidget {
context
.read<SortEditorBloc>()
.add(const SortEditorEvent.deleteAllSorts());
PopoverContainer.of(context).close();
},
leftIcon: const FlowySvg(FlowySvgs.delete_s),
),
@@ -8,4 +8,6 @@ class SortInfo {
SortInfo({required this.sortPB, required this.fieldInfo});
String get sortId => sortPB.id;
String get fieldId => sortPB.fieldId;
}
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/share_button.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
@@ -212,4 +213,16 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
@override
List<NavigationItem> get navigationItems => [this];
@override
Widget? get rightBarItem {
return Row(
children: [
DatabaseShareButton(
key: ValueKey(notifier.view.id),
view: notifier.view,
),
],
);
}
}
@@ -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,86 +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) {
dateStr = "${cellData.date} ${cellData.time}";
}
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_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();
}
}
@@ -10,6 +10,7 @@ import 'cells/date_card_cell.dart';
import 'cells/number_card_cell.dart';
import 'cells/select_option_card_cell.dart';
import 'cells/text_card_cell.dart';
import 'cells/timestamp_card_cell.dart';
import 'cells/url_card_cell.dart';
// T represents as the Generic card data
@@ -39,13 +40,23 @@ class CardCellBuilder<CustomCardData> {
key: key,
);
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return DateCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.DateTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.LastEditedTime:
return TimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.CreatedTime:
return TimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.CreatedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.SingleSelect:
return SelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:flutter/material.dart';
typedef CellRenderHook<C, CustomCardData> = Widget? Function(
@@ -50,6 +51,16 @@ class RowCardRenderHook<CustomCardData> {
renderHook[FieldType.DateTime] = _typeSafeHook<DateCellDataPB>(hook);
}
/// Add a render hook for [FieldType.LastEditedTime] and [FieldType.CreatedTime]
void addTimestampCellHook(
CellRenderHook<TimestampCellDataPB, CustomCardData?> hook,
) {
renderHook[FieldType.LastEditedTime] =
_typeSafeHook<TimestampCellDataPB>(hook);
renderHook[FieldType.CreatedTime] =
_typeSafeHook<TimestampCellDataPB>(hook);
}
CellRenderHook<dynamic, CustomCardData> _typeSafeHook<C>(
CellRenderHook<C, CustomCardData?> hook,
) {
@@ -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,11 +31,17 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
builder: (context, state) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: ChecklistProgressBar(percent: state.percent),
),
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.allOptions.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: ChecklistProgressBar(percent: state.percent),
);
},
),
);
}
@@ -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,
),
),
Loaded 100 of 674 files, more files were not shown because too many files have changed in this diff. Show more