Compare commits

...
72 Commits
Author SHA1 Message Date
Mathias Mogensen e125e9493a fix: offset row actions by cover height (#6390) 2024-09-24 14:33:23 +02:00
Lucas 5fbd07e96a fix: potential ai_repository not init (#6388)
* fix: potential ai_repository not init

* fix: assertion when deleting block

* chore: remove the rule

* test: add smart edit bloc test

* test: add delete block test

* chore: remove unused_import

* fix: paste image integration test
2024-09-24 20:24:28 +08:00
Mathias Mogensen 0a6bee6232 fix: hover bug in row banner (#6389) 2024-09-24 13:57:26 +02:00
Mathias Mogensen 552f2a73de fix: row cover improvements (#6381)
* fix: row cover improvements

* feat: set image from media cell as cover

* fix: duplicate row meta when duplicating row

* fix: use serialize repr and deserialize repr

* chore: update collab revision

* fix: failing test and bug w/ document icon

* fix: show empty cover on load failure

* fix: tauri collab revision
2024-09-23 16:28:47 +02:00
Richard Shiue eeb6b69f58 fix: semi-transparent colors being forced to full opacity in FlowySvg (#6376)
* fix: semi-transparent colors being forced to full opacity

* chore: code style

* chore: update comment
2024-09-23 20:44:23 +08:00
Richard Shiue 99889b9950 fix(flutter_desktop): select option kanban groups not updating #6369 2024-09-23 20:44:10 +08:00
Lucas c51b495544 feat: support appflowy cloud in sign-in settings page (#6386)
* feat: support appflowy cloud in sign-in settings page

* test: add cloud server test

* test: wait until the tooltip disappear
2024-09-23 20:19:29 +08:00
Lucas 0d2841227a feat: add numpadSubtract as zoom out keycode (#6380)
* feat: add numpadSubtract as zoom out keycode

* test: add zoom in/out and reset zoom refacotr test

* fix: zoom in test assertion

* fix: zoom in/out test

* test: exclude LogicalKeyboardKey.add & LogicalKeyboardKey.numpadSubtract in linux test
2024-09-23 12:25:45 +08:00
Lucas 2dc7dc1a26 fix: sidebar move page test (#6377) 2024-09-22 14:13:44 +08:00
Lucas f0a494b41d fix: 0.7.0 launch review issues (#6365)
* fix: cannot use 'move to' from sidebar

* feat: disable moving the page to a database

* fix: check if the move to path is same as current path

* fix: document width doesn't refresh after resetting

* fix: disable tooltip when dragging blocks

* fix: reminder not render when dragging

* chore: optimize code

* fix: after changing the language in the settings page, some of the text doesn't change

* feat: support moving a block to another block's child

* fix: tooltip assertion

* fix: disable table cell block

* chore: optimize moving logic

* test: collaborative workspace test

* test: move to page test

* test: drag block test

* test: check if the tooltip is hidden when dragging block

* test: move a block to another block's child

* test: change document width test
2024-09-22 09:35:11 +08:00
Richard Shiue 806fc75a8a fix: launch review issues 0.7.0 (#6367)
* fix: leave away the property edit action -> the menu doesnt close

* fix: tooltip showing when hovering over empty space

* fix: text not centered

* fix: board card sizing issue
2024-09-22 09:34:21 +08:00
Nathan.fooo d3362bf96a chore: fix anon test (#6375)
* chore: fix anon test

* chore: fix test
2024-09-21 16:04:18 +08:00
Mathias Mogensen 7bef698f03 fix: hide line numbers in code block (#6352) 2024-09-21 16:00:23 +08:00
Kilu.He bea4b60a7a fix: fix jump error issue (#6374) 2024-09-21 15:08:02 +08:00
Kilu.He 1100c8b7e1 fix: modified row page cover (#6373) 2024-09-21 14:47:33 +08:00
Kilu.He 728c75ff87 fix: adjust row page skeleton (#6372) 2024-09-21 14:42:45 +08:00
Kilu.He e01a59c772 fix: adjust editor's margin bottom (#6371) 2024-09-21 14:39:58 +08:00
Kilu.He 164933325c fix: adjust margin bottom (#6370) 2024-09-21 14:31:10 +08:00
Kilu.He 88df232139 feat: support app router (#6366)
* fix: modified the folder name

* feat: open document and database

* feat: support editing

* fix: lint

* fix: add share actions

* fix: support interfaces
2024-09-21 10:39:09 +08:00
Nathan.fooo 7d3159c8da fix: delete duplicate test in board (#6364)
* chore: fix delete duplicate test in board

* chore: update log
2024-09-21 09:57:17 +08:00
Nathan.fooo e36f9aee4e chore: bump collab version (#6363) 2024-09-20 16:19:04 +08:00
Jessie.H 0a5f98014c chore: update zh-TW translations (#6357) 2024-09-20 10:01:19 +08:00
Mathias Mogensen e5cb16f5b3 chore: add changelog for 0.7.0 (#6355) 2024-09-19 08:15:34 +02:00
Lucas aa06c78b68 fix: settings page minor issues (#6354) 2024-09-19 13:40:08 +08:00
Lucas da03f40d29 fix: icon doesn't align with the editor when the editor is not initialized (#6345) 2024-09-19 10:57:06 +08:00
Mathias Mogensen 6ddbb418eb fix: image upload loading (#6351) 2024-09-19 00:33:13 +02:00
Nathan.fooo f9f5ae04d0 chore: fix potential when open a workspace with retry will cause lock… (#6344)
* chore: fix potential when open a workspace with retry will cause lock issue

* chore: disable verbose log
2024-09-18 21:00:01 +08:00
Richard Shiue 71e7f54367 fix(flutter_desktop): alignment on filter and sort menu (#6337) 2024-09-18 20:28:13 +08:00
Lucas c5afbb97a0 feat: support settings on sign in page (#6342)
* feat: support settings on sign in page

* feat: add settings entry in sign in page

* feat: support exporting log files on desktop

* chore: remove openfile dependency
2024-09-18 18:03:03 +08:00
Chikage0o0 d189c1ca4e chore: update zh-CN translations (#6281)
fix chinese translation
2024-09-18 14:45:58 +08:00
Lucas 630fdb8995 feat: support moveTo feature in more action menu (#6338)
* feat: support moveTo feature in more action menu

* fix: unable to switch to another workspace

* fix: integration test

* chore: update editor version

* fix: integration test
2024-09-18 14:45:25 +08:00
Lucas 522143cfd8 feat: support adjusting document width (#6327)
* feat: support adjusting document width

* chore: refactor padding calculation function

* fix: remove reset confim dialog

* chore: remove unused code

* feat: update sider position

* feat: use max width instead of padding

* fix: remove unused code

* fix: padding is negative error

* fix: set maxDocumentWidth to 480 * 4 and minDocumentWidth to 480

* fix: integration test
2024-09-18 10:43:23 +08:00
Richard Shiue 1dedb909e5 fix(flutter_desktop): incorrect row meta in row detail page (#6329) 2024-09-17 21:49:52 +08:00
LucasandMathias Mogensen 605a079927 fix: inputting backtick will crash when using French IME (#6321)
* fix: inputting backtick will crash when using French IME

* feat: convert backtick to codeblock

* chore: update frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/backtick_character_command.dart

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2024-09-17 14:12:17 +08:00
Lucas 938abb396e fix: dismiss settings dialog after signing in (#6324)
* fix: dismiss settings dialog after signing in

* fix: tests

* fix: use boardcast stream to allow multiple listener
2024-09-17 13:19:15 +08:00
Richard Shiue a406dfbc43 fix(flutter_desktop): make icon of primary cells align to top (#6328)
* fix: make icon of primary cells align to top

* chore: use FlowyText.emoji
2024-09-17 11:24:34 +08:00
Kilu.He f303674737 fix: fixed image size for media cell (#6325) 2024-09-16 23:24:09 +08:00
Mathias Mogensen 2bc9753f60 fix: show count of files on card per column (#6323)
* fix: show count of files on card per column

* fix: analyzer
2024-09-16 15:18:14 +02:00
Richard Shiue 56cc7bb972 test(flutter_desktop): add grid integration tests (#6298)
* test: run board integration tests

* test: add grid integration tests

* test: fix delete group test

* chore: fix rust-lib tests

* chore: comment out failing test fix tests and remove tOdOS
2024-09-16 21:01:08 +08:00
Richard Shiue 710e894446 fix: grid filters (#6317) 2024-09-16 17:34:02 +08:00
Lucas d67d77f442 feat: support dragging the block to reorder (#6285)
* feat: support dragging the block to reorder

* feat: render feedback widget

* feat: add drag to move translation

* fix: the feedback widget doesn't update after node changed

* feat: render table placeholder

* feat: implement auto scroll when dragging to edge

* chore: add back the drop images/files feature

* chore: code refactor

* feat: exclude image and file block

* feat: exclude image gallery and database block
2024-09-16 17:01:02 +08:00
nathan 129db6925c chore: clippy 2024-09-16 16:53:47 +08:00
nathan 924df56d28 chore: clippy 2024-09-16 16:07:29 +08:00
Nathan.fooo c2b194b1d2 chore: remove cancel get rows (#6320)
* chore: config flutter logs

* chore: remove cancellation when get rows

* chore: clippy
2024-09-16 15:46:18 +08:00
Kilu.He 68986bd170 fix: modified primary cell (#6319) 2024-09-16 15:13:22 +08:00
Kilu.He e4029d05dc feat: support file media on web (#6318) 2024-09-16 14:24:25 +08:00
Nathan.foooandRichard Shiue f1bc9f860c fix: untitled override the true value in row details page (#6315)
* fix: untitled override the true value in row details page

* chore: use on complete edit callback

* chore: fix test

* chore: fix test

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2024-09-16 13:19:36 +08:00
Lucas b865037728 chore: downgrade appimage builder (#6291) 2024-09-16 09:32:43 +08:00
Lucas 7e0de1f69c fix: view title doesn't update after renaming from 'more' button (#6309) 2024-09-16 09:32:32 +08:00
Mathias Mogensen 12c575dbbe feat: cover in database row (#6238)
* feat: cover in database

* chore: clean debugPrint

* chore: update collab rev

* feat: add cover functionality

* fix: provider issue for calendarbloc in popover

* feat: mobile row cover

* fix: imports after merge

* test: add media test to runner

* fix: mobile cover

* test: add row cover tests

* fix: add isClosed check
2024-09-15 22:33:18 +02:00
Mathias Mogensen f44fb02e4a fix: provider issue in row document (#6289)
* fix: provider issue in row document

* fix: use bloc consumer
2024-09-15 22:33:00 +02:00
Richard Shiue 4df59ca421 fix(flutter_desktop): reordering or creating a row while sorts are active (#6306)
* fix: reordering or creating a row while sorts are active

* test: fix integration test
2024-09-15 23:46:41 +08:00
Richard Shiue 7fee00ab72 chore(flutter_desktop): show full name of groups on hover (#6307)
* chore: show full name of groups on hover

* fix: elide text on overflow
2024-09-15 23:46:22 +08:00
Nathan.fooo 7271e0a108 chore: update ai model name (#6316) 2024-09-15 23:36:29 +08:00
Nathan.fooo 877236baf9 fix: sort with original row orders (#6314)
* fix: sort with original row orders

* chore: bump collab
2024-09-15 19:14:16 +08:00
Kilu.He 2c66634c89 feat: support invitation page (#6300) 2024-09-15 13:45:13 +08:00
Nathan.fooo 609ea27c42 fix: write/load database/document collab (#6308)
* chore: post document collab after create

* chore: write database rows

* chore: fix test

* chore: fix test

* chore: bump collab

* chore: fix get related row

* chore: try to fix open database error

* chore: update client api
2024-09-15 00:02:17 +08:00
Nathan.fooo aafdb80455 chore: bump collab that removes timeout when loading row (#6296) 2024-09-13 14:08:39 +08:00
Mathias Mogensen 4888d5d528 fix: media option improvements (#6260)
* feat: hide file names option

* chore: copywriting fixes

* fix: mobile cell

* feat: browse all images in a cell in viewer

* chore: unused variable

* fix: dialogs on download file

* feat: upload multiple files at once

* test: improve test coverage

* fix: missing format of rust files from merge
2024-09-13 00:12:13 +02:00
Nathan.fooo 21cc130e4e chore: unfinalized rows (#6283)
* chore: unfinalized rows

* chore: blocking read

* chore: fix test

* chore: clippy
2024-09-12 22:27:11 +08:00
AnnieandLucas.Xu a02de1a435 chore: update README.md (#6286)
* chore: Update README.md

added links to App Store and Play Store

* Update README.md

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-09-12 21:23:41 +08:00
Mathias Mogensen 477683d454 fix: card rebuild issue (#6277) 2024-09-12 15:27:32 +08:00
Lucas.Xu 340f37e00d chore: update appflowy_editor version (#6282)
* chore: update connectivity_plus

* chore: update appflowy_editor

* chore: enable editor log when enableDocumentInternalLog is true

* chore: update podfile.lock

* Revert "chore: update podfile.lock"

This reverts commit 41dbcad8c1.

* fix: compile

* chore: revert hotkey changes

* chore: revert network_monitor change
2024-09-12 14:40:19 +08:00
Richard Shiue eea3004a97 feat(flutter_desktop): colored column names in kanban board (#6270)
* feat: colored column names in kanban board

* fix: rename group event

* chore: add comment for rough design

* chore: revert unintentional changes

* fix: use new colors and use new delete confirmation dialog
2024-09-12 09:32:23 +08:00
Nathan.fooo f59772756a chore: adjust num of preload rows (#6280)
* chore: concurrent load rows

* chore: update client api

* chore: fix build
2024-09-12 08:47:49 +08:00
Lucas.Xu 1cf4f493db feat: ai writer on multi-lines of text (#6267)
* feat: ai writer on multi-lines of text

* feat: use flutter_bloc to manage ai response

* feat: integrate smart_edit_bloc

* fix: bloc not found

* feat: support 'fix spelling and grammer'

* fix: add log to address ai generate issue
2024-09-11 20:15:27 +08:00
Kilu.He c5a9dea721 feat: update iOS download link (#6276) 2024-09-11 11:24:01 +08:00
Nathan.fooo 7d2719dd71 fix: filter rows sometime not working (#6272) 2024-09-10 23:41:06 +08:00
Lucas.Xu 2ef896fb4a feat: support moving cursor using keyboard gesture on mobile (#6239)
* fix: invalid value when inserting image

* feat: support moving cursor using keyboard gesture on mobile

* fix: invalid value when inserting image
2024-09-10 16:33:16 +08:00
Lucas.Xu a0c6fb2fe8 fix: the last block of a doc is randomly moved to the first position of the doc (#6264) 2024-09-10 16:32:51 +08:00
Mathias Mogensen ee3054b355 fix: more default image extensions (#6250)
* fix: more default image extensions

* fix: add and comment default image extensions
2024-09-10 16:32:36 +08:00
Lucas.Xu bfe6d25322 chore: update version to 0.7.0 (#6266) 2024-09-10 16:32:14 +08:00
594 changed files with 19102 additions and 7797 deletions

No files matched your search

+26
View File
@@ -1,4 +1,30 @@
# Release Notes
## Version 0.7.0 - 19/09/2024
### New Features
- Support reordering blocks in document with drag and drop
- Support for adding a cover to a row/card in databases
- Added support for accessing settings on the sign-in page
- Added "Move to" option to the document menu in top right corner
- Support for adjusting the document width from settings
- Show full name of a group on hover
- Colored group names in kanban boards
- Support "Ask AI" on multiple lines of text
- Support for keyboard gestures to move cursor on Mobile
- Added markdown support for quickly inserting a code block using three backticks
### Bug Fixes
- Fixed a critical bug where the backtick character would crash the application
- Fixed an issue with signing-in from the settings dialog where the dialog would persist
- Fixed a visual bug with icon alignment in primary cell of database rows
- Fixed a bug with filters applied where new rows were inserted in wrong position
- Fixed a bug where "Untitled" would override the name of the row
- Fixed page title not updating after renaming from "More"-menu
- Fixed File block breaking row detail document
- Fixed issues with reordering rows with sorting rules applied
- Improvements to the File & Media type in Database
- Performance improvement in Grid view
- Fixed filters sometimes not applying properly in databases
## Version 0.6.9 - 09/09/2024
### New Features
- Added a new property type, 'Files & media'
+2 -2
View File
@@ -44,8 +44,8 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases)
- Other channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
- Available on
- App Store: iPhone
- Play Store: Android 10 or above; ARMv7 is not supported
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is not supported
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.9"
APPFLOWY_VERSION = "0.7.0"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -0,0 +1,14 @@
"{""id"":""RGmzka"",""name"":""Name"",""field_type"":0,""type_options"":{""0"":{""data"":""""}},""is_primary"":true}","{""id"":""oYoH-q"",""name"":""Time Slot"",""field_type"":2,""type_options"":{""0"":{""date_format"":3,""data"":"""",""time_format"":1,""timezone_id"":""""},""2"":{""date_format"":3,""time_format"":1,""timezone_id"":""""}},""is_primary"":false}","{""id"":""zVrp17"",""name"":""Amount"",""field_type"":1,""type_options"":{""1"":{""scale"":0,""format"":4,""name"":""Number"",""symbol"":""RUB""},""0"":{""data"":"""",""symbol"":""RUB"",""name"":""Number"",""format"":0,""scale"":0}},""is_primary"":false}","{""id"":""_p4EGt"",""name"":""Delta"",""field_type"":1,""type_options"":{""1"":{""name"":""Number"",""format"":36,""symbol"":""RUB"",""scale"":0},""0"":{""data"":"""",""symbol"":""RUB"",""name"":""Number"",""format"":0,""scale"":0}},""is_primary"":false}","{""id"":""Z909lc"",""name"":""Email"",""field_type"":6,""type_options"":{""6"":{""url"":"""",""content"":""""},""0"":{""data"":"""",""content"":"""",""url"":""""}},""is_primary"":false}","{""id"":""dBrSc7"",""name"":""Registration Complete"",""field_type"":5,""type_options"":{""5"":{}},""is_primary"":false}","{""id"":""VoigvK"",""name"":""Progress"",""field_type"":7,""type_options"":{""0"":{""data"":""""},""7"":{}},""is_primary"":false}","{""id"":""gbbQwh"",""name"":""Attachments"",""field_type"":14,""type_options"":{""0"":{""data"":"""",""content"":""{\""files\"":[]}""},""14"":{""content"":""{\""files\"":[]}""}},""is_primary"":false}","{""id"":""id3L0G"",""name"":""Priority"",""field_type"":3,""type_options"":{""3"":{""content"":""{\""options\"":[{\""id\"":\""cplL\"",\""name\"":\""VIP\"",\""color\"":\""Purple\""},{\""id\"":\""GSf_\"",\""name\"":\""High\"",\""color\"":\""Blue\""},{\""id\"":\""qnja\"",\""name\"":\""Medium\"",\""color\"":\""Green\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""541SFC"",""name"":""Tags"",""field_type"":4,""type_options"":{""0"":{""data"":"""",""content"":""{\""options\"":[],\""disable_color\"":false}""},""4"":{""content"":""{\""options\"":[{\""id\"":\""1i4f\"",\""name\"":\""Education\"",\""color\"":\""Yellow\""},{\""id\"":\""yORP\"",\""name\"":\""Health\"",\""color\"":\""Orange\""},{\""id\"":\""SEUo\"",\""name\"":\""Hobby\"",\""color\"":\""LightPink\""},{\""id\"":\""uRAO\"",\""name\"":\""Family\"",\""color\"":\""Pink\""},{\""id\"":\""R9I7\"",\""name\"":\""Work\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""lg0B7O"",""name"":""Last modified"",""field_type"":8,""type_options"":{""0"":{""time_format"":1,""field_type"":8,""date_format"":3,""data"":"""",""include_time"":true},""8"":{""date_format"":3,""field_type"":8,""time_format"":1,""include_time"":true}},""is_primary"":false}","{""id"":""5riGR7"",""name"":""Created at"",""field_type"":9,""type_options"":{""0"":{""field_type"":9,""include_time"":true,""date_format"":3,""time_format"":1,""data"":""""},""9"":{""include_time"":true,""field_type"":9,""date_format"":3,""time_format"":1}},""is_primary"":false}"
"{""data"":""Olaf"",""created_at"":1726063289,""last_modified"":1726063289,""field_type"":0}","{""last_modified"":1726122374,""created_at"":1726110045,""reminder_id"":"""",""is_range"":true,""include_time"":true,""end_timestamp"":""1725415200"",""field_type"":2,""data"":""1725256800""}","{""field_type"":1,""data"":""55200"",""last_modified"":1726063592,""created_at"":1726063592}","{""last_modified"":1726062441,""created_at"":1726062441,""data"":""0.5"",""field_type"":1}","{""created_at"":1726063719,""last_modified"":1726063732,""data"":""doyouwannabuildasnowman@arendelle.gov"",""field_type"":6}",,"{""field_type"":7,""last_modified"":1726064207,""data"":""{\""options\"":[{\""id\"":\""oqXQ\"",\""name\"":\""find elsa\"",\""color\"":\""Purple\""},{\""id\"":\""eQwp\"",\""name\"":\""find anna\"",\""color\"":\""Purple\""},{\""id\"":\""5-B3\"",\""name\"":\""play in the summertime\"",\""color\"":\""Purple\""},{\""id\"":\""UBFn\"",\""name\"":\""get a personal flurry\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""oqXQ\"",\""eQwp\"",\""UBFn\""]}"",""created_at"":1726064129}",,"{""created_at"":1726065208,""data"":""cplL"",""last_modified"":1726065282,""field_type"":3}","{""field_type"":4,""data"":""1i4f"",""last_modified"":1726105102,""created_at"":1726105102}","{""field_type"":8,""data"":""1726122374""}","{""data"":""1726060476"",""field_type"":9}"
"{""field_type"":0,""last_modified"":1726063323,""data"":""Beatrice"",""created_at"":1726063323}",,"{""last_modified"":1726063638,""data"":""828600"",""created_at"":1726063607,""field_type"":1}","{""field_type"":1,""created_at"":1726062488,""data"":""-2.25"",""last_modified"":1726062488}","{""last_modified"":1726063790,""data"":""btreee17@gmail.com"",""field_type"":6,""created_at"":1726063790}","{""created_at"":1726062718,""data"":""Yes"",""field_type"":5,""last_modified"":1726062724}","{""created_at"":1726064277,""data"":""{\""options\"":[{\""id\"":\""BDuH\"",\""name\"":\""get the leaf node\"",\""color\"":\""Purple\""},{\""id\"":\""GXAr\"",\""name\"":\""upgrade to b+\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""field_type"":7,""last_modified"":1726064293}",,"{""data"":""GSf_"",""created_at"":1726065288,""last_modified"":1726065288,""field_type"":3}","{""created_at"":1726105110,""data"":""yORP,uRAO"",""last_modified"":1726105111,""field_type"":4}","{""data"":""1726105111"",""field_type"":8}","{""field_type"":9,""data"":""1726060476""}"
"{""last_modified"":1726063355,""created_at"":1726063355,""field_type"":0,""data"":""Lancelot""}","{""data"":""1726468159"",""is_range"":true,""end_timestamp"":""1726727359"",""reminder_id"":"""",""include_time"":false,""field_type"":2,""created_at"":1726122403,""last_modified"":1726122559}","{""created_at"":1726063617,""last_modified"":1726063617,""data"":""22500"",""field_type"":1}","{""data"":""11.6"",""last_modified"":1726062504,""field_type"":1,""created_at"":1726062504}","{""field_type"":6,""data"":""sir.lancelot@gmail.com"",""last_modified"":1726063812,""created_at"":1726063812}","{""data"":""No"",""field_type"":5,""last_modified"":1726062724,""created_at"":1726062375}",,,"{""data"":""cplL"",""created_at"":1726065286,""last_modified"":1726065286,""field_type"":3}","{""last_modified"":1726105237,""data"":""SEUo"",""created_at"":1726105237,""field_type"":4}","{""field_type"":8,""data"":""1726122559""}","{""field_type"":9,""data"":""1726060476""}"
"{""data"":""Scotty"",""last_modified"":1726063399,""created_at"":1726063399,""field_type"":0}","{""reminder_id"":"""",""last_modified"":1726122418,""include_time"":true,""data"":""1725868800"",""end_timestamp"":""1726646400"",""created_at"":1726122381,""field_type"":2,""is_range"":true}","{""created_at"":1726063650,""last_modified"":1726063650,""data"":""10900"",""field_type"":1}","{""data"":""0"",""created_at"":1726062581,""last_modified"":1726062581,""field_type"":1}","{""last_modified"":1726063835,""created_at"":1726063835,""field_type"":6,""data"":""scottylikestosing@outlook.com""}","{""data"":""Yes"",""field_type"":5,""created_at"":1726062718,""last_modified"":1726062718}","{""created_at"":1726064309,""data"":""{\""options\"":[{\""id\"":\""Cw0K\"",\""name\"":\""vocal warmup\"",\""color\"":\""Purple\""},{\""id\"":\""nYMo\"",\""name\"":\""mixed voice training\"",\""color\"":\""Purple\""},{\""id\"":\""i-OX\"",\""name\"":\""belting training\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""Cw0K\"",\""nYMo\"",\""i-OX\""]}"",""field_type"":7,""last_modified"":1726064325}","{""last_modified"":1726122911,""created_at"":1726122835,""data"":[""{\""id\"":\""746a741d-98f8-4cc6-b807-a82d2e78c221\"",\""name\"":\""googlelogo_color_272x92dp.png\"",\""url\"":\""https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}"",""{\""id\"":\""cbbab3ee-32ab-4438-a909-3f69f935a8bd\"",\""name\"":\""tL_v571NdZ0.svg\"",\""url\"":\""https://static.xx.fbcdn.net/rsrc.php/y9/r/tL_v571NdZ0.svg\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Link\""}""],""field_type"":14}",,"{""data"":""SEUo,yORP"",""field_type"":4,""last_modified"":1726105123,""created_at"":1726105115}","{""data"":""1726122911"",""field_type"":8}","{""data"":""1726060539"",""field_type"":9}"
"{""field_type"":0,""created_at"":1726063405,""last_modified"":1726063421,""data"":""""}",,,"{""last_modified"":1726062625,""field_type"":1,""data"":"""",""created_at"":1726062607}",,"{""data"":""No"",""last_modified"":1726062702,""created_at"":1726062393,""field_type"":5}",,,,,"{""data"":""1726063421"",""field_type"":8}","{""data"":""1726060539"",""field_type"":9}"
"{""field_type"":0,""data"":""Thomas"",""last_modified"":1726063421,""created_at"":1726063421}","{""reminder_id"":"""",""field_type"":2,""data"":""1725627600"",""is_range"":false,""created_at"":1726122583,""last_modified"":1726122593,""end_timestamp"":"""",""include_time"":true}","{""last_modified"":1726063666,""field_type"":1,""data"":""465800"",""created_at"":1726063666}","{""last_modified"":1726062516,""field_type"":1,""created_at"":1726062516,""data"":""-0.03""}","{""field_type"":6,""last_modified"":1726063848,""created_at"":1726063848,""data"":""tfp3827@gmail.com""}","{""field_type"":5,""last_modified"":1726062725,""data"":""Yes"",""created_at"":1726062376}","{""created_at"":1726064344,""data"":""{\""options\"":[{\""id\"":\""D6X8\"",\""name\"":\""brainstorm\"",\""color\"":\""Purple\""},{\""id\"":\""XVN9\"",\""name\"":\""schedule\"",\""color\"":\""Purple\""},{\""id\"":\""nJx8\"",\""name\"":\""shoot\"",\""color\"":\""Purple\""},{\""id\"":\""7Mrm\"",\""name\"":\""edit\"",\""color\"":\""Purple\""},{\""id\"":\""o6vg\"",\""name\"":\""publish\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""D6X8\""]}"",""last_modified"":1726064379,""field_type"":7}",,"{""last_modified"":1726065298,""created_at"":1726065298,""field_type"":3,""data"":""GSf_""}","{""data"":""yORP,SEUo"",""field_type"":4,""last_modified"":1726105229,""created_at"":1726105229}","{""data"":""1726122593"",""field_type"":8}","{""field_type"":9,""data"":""1726060540""}"
"{""data"":""Juan"",""last_modified"":1726063423,""created_at"":1726063423,""field_type"":0}","{""created_at"":1726122510,""reminder_id"":"""",""include_time"":false,""is_range"":true,""last_modified"":1726122515,""data"":""1725604115"",""end_timestamp"":""1725776915"",""field_type"":2}","{""field_type"":1,""created_at"":1726063677,""last_modified"":1726063677,""data"":""93100""}","{""field_type"":1,""data"":""4.86"",""created_at"":1726062597,""last_modified"":1726062597}",,"{""last_modified"":1726062377,""field_type"":5,""data"":""Yes"",""created_at"":1726062377}","{""last_modified"":1726064412,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""tTDq\"",\""name\"":\""complete onboarding\"",\""color\"":\""Purple\""},{\""id\"":\""E8Ds\"",\""name\"":\""contact support\"",\""color\"":\""Purple\""},{\""id\"":\""RoGN\"",\""name\"":\""get started\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""tTDq\"",\""E8Ds\""]}"",""created_at"":1726064396}",,"{""created_at"":1726065278,""field_type"":3,""data"":""qnja"",""last_modified"":1726065278}","{""data"":""R9I7,yORP,1i4f"",""field_type"":4,""created_at"":1726105126,""last_modified"":1726105127}","{""data"":""1726122515"",""field_type"":8}","{""data"":""1726060541"",""field_type"":9}"
"{""data"":""Alex"",""created_at"":1726063432,""last_modified"":1726063432,""field_type"":0}","{""reminder_id"":"""",""data"":""1725292800"",""include_time"":true,""last_modified"":1726122448,""created_at"":1726122422,""is_range"":true,""end_timestamp"":""1725551940"",""field_type"":2}","{""field_type"":1,""last_modified"":1726063683,""created_at"":1726063683,""data"":""3560""}","{""created_at"":1726062561,""data"":""1.96"",""last_modified"":1726062561,""field_type"":1}","{""last_modified"":1726063952,""created_at"":1726063931,""data"":""al3x1343@protonmail.com"",""field_type"":6}","{""last_modified"":1726062375,""field_type"":5,""created_at"":1726062375,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""qNyr\"",\""name\"":\""finish reading book\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064616,""last_modified"":1726064616,""field_type"":7}",,"{""data"":""qnja"",""created_at"":1726065272,""last_modified"":1726065272,""field_type"":3}","{""created_at"":1726105180,""last_modified"":1726105180,""field_type"":4,""data"":""R9I7,1i4f""}","{""field_type"":8,""data"":""1726122448""}","{""field_type"":9,""data"":""1726060541""}"
"{""last_modified"":1726063478,""created_at"":1726063436,""field_type"":0,""data"":""Alexander""}",,"{""field_type"":1,""last_modified"":1726063691,""created_at"":1726063691,""data"":""2073""}","{""field_type"":1,""data"":""0.5"",""last_modified"":1726062577,""created_at"":1726062577}","{""last_modified"":1726063991,""field_type"":6,""created_at"":1726063991,""data"":""alexandernotthedra@gmail.com""}","{""field_type"":5,""last_modified"":1726062378,""created_at"":1726062377,""data"":""No""}",,,"{""created_at"":1726065291,""data"":""GSf_"",""last_modified"":1726065291,""field_type"":3}","{""last_modified"":1726105142,""created_at"":1726105133,""data"":""SEUo"",""field_type"":4}","{""field_type"":8,""data"":""1726105142""}","{""field_type"":9,""data"":""1726060542""}"
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://static.cdninstagram.com/rsrc.php/v3/ym/r/BQdTmxpRI6f.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
"{""field_type"":0,""last_modified"":1726063467,""data"":""Joanna"",""created_at"":1726063467}","{""include_time"":false,""end_timestamp"":""1727072893"",""is_range"":true,""last_modified"":1726122493,""created_at"":1726122483,""data"":""1726554493"",""field_type"":2,""reminder_id"":""""}","{""last_modified"":1726065463,""data"":""16470"",""field_type"":1,""created_at"":1726065463}","{""created_at"":1726062626,""field_type"":1,""last_modified"":1726062626,""data"":""-5.36""}","{""last_modified"":1726064069,""data"":""joannastrawberry29+hello@gmail.com"",""created_at"":1726064069,""field_type"":6}",,"{""field_type"":7,""created_at"":1726064444,""last_modified"":1726064460,""data"":""{\""options\"":[{\""id\"":\""ZxJz\"",\""name\"":\""post on Twitter\"",\""color\"":\""Purple\""},{\""id\"":\""upwi\"",\""name\"":\""watch Youtube videos\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""upwi\""]}""}",,"{""created_at"":1726065317,""last_modified"":1726065317,""field_type"":3,""data"":""qnja""}","{""field_type"":4,""last_modified"":1726105173,""data"":""uRAO,yORP"",""created_at"":1726105170}","{""data"":""1726122493"",""field_type"":8}","{""data"":""1726060545"",""field_type"":9}"
"{""last_modified"":1726063457,""created_at"":1726063457,""data"":""George"",""field_type"":0}","{""include_time"":true,""reminder_id"":"""",""field_type"":2,""is_range"":true,""created_at"":1726122521,""end_timestamp"":""1725829200"",""data"":""1725822900"",""last_modified"":1726122535}","{""last_modified"":1726065493,""field_type"":1,""data"":""9500"",""created_at"":1726065493}","{""last_modified"":1726062680,""created_at"":1726062680,""field_type"":1,""data"":""1.7""}","{""data"":""plgeorgebball@gmail.com"",""field_type"":6,""last_modified"":1726064087,""created_at"":1726064036}",,"{""last_modified"":1726064513,""data"":""{\""options\"":[{\""id\"":\""zy0x\"",\""name\"":\""game vs celtics\"",\""color\"":\""Purple\""},{\""id\"":\""WJsv\"",\""name\"":\""training\"",\""color\"":\""Purple\""},{\""id\"":\""w-f8\"",\""name\"":\""game vs spurs\"",\""color\"":\""Purple\""},{\""id\"":\""p1VQ\"",\""name\"":\""game vs knicks\"",\""color\"":\""Purple\""},{\""id\"":\""VjUA\"",\""name\"":\""recovery\"",\""color\"":\""Purple\""},{\""id\"":\""sQ8X\"",\""name\"":\""don't get injured\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064486,""field_type"":7}",,"{""field_type"":3,""last_modified"":1726065310,""data"":""qnja"",""created_at"":1726065310}","{""created_at"":1726105205,""field_type"":4,""last_modified"":1726105249,""data"":""R9I7,1i4f,yORP,SEUo""}","{""data"":""1726122535"",""field_type"":8}","{""field_type"":9,""data"":""1726060546""}"
"{""data"":""Judy"",""created_at"":1726063475,""field_type"":0,""last_modified"":1726063487}","{""end_timestamp"":"""",""reminder_id"":"""",""data"":""1726640950"",""field_type"":2,""include_time"":false,""created_at"":1726122550,""last_modified"":1726122550,""is_range"":false}",,,"{""created_at"":1726063882,""field_type"":6,""last_modified"":1726064000,""data"":""judysmithjr@outlook.com""}","{""last_modified"":1726062712,""field_type"":5,""data"":""Yes"",""created_at"":1726062712}","{""created_at"":1726064549,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""j8cC\"",\""name\"":\""finish training\"",\""color\"":\""Purple\""},{\""id\"":\""SmSk\"",\""name\"":\""brainwash\"",\""color\"":\""Purple\""},{\""id\"":\""mnf5\"",\""name\"":\""welcome to ba sing se\"",\""color\"":\""Purple\""},{\""id\"":\""hcrj\"",\""name\"":\""don't mess up\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""j8cC\"",\""SmSk\"",\""mnf5\"",\""hcrj\""]}"",""last_modified"":1726064591}",,,"{""field_type"":4,""last_modified"":1726105152,""created_at"":1726105152,""data"":""R9I7""}","{""field_type"":8,""data"":""1726122550""}","{""field_type"":9,""data"":""1726060549""}"
@@ -38,7 +38,7 @@ void main() {
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
tester.expectToSeeText(LocaleKeys.signIn_loginStartWithAnonymous.tr());
await tester.tapContinousAnotherWay();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
@@ -75,18 +75,6 @@ void main() {
await tester.logout();
await tester.pumpAndSettle();
// tap the continue as anonymous button
await tester
.tapButton(find.text(LocaleKeys.signIn_loginStartWithAnonymous.tr()));
await tester.expectToSeeHomePage();
// New anon user name
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
final userNameInput =
tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile;
expect(userNameInput.name, 'Me');
});
});
}
@@ -86,10 +86,13 @@ void main() {
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.cloud);
await tester.pumpAndSettle();
// the switch should be on by default
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
// wait for the switch animation
await tester.wait(250);
// the switch should be off
tester.assertAppFlowyCloudEnableSyncSwitchValue(false);
@@ -97,6 +100,7 @@ void main() {
// the switch should be on after toggling
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
await tester.wait(250);
});
});
}
@@ -1,11 +1,14 @@
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'document/document_drag_block_test.dart' as document_drag_block_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'user_setting_sync_test.dart' as user_sync_test;
import 'workspace/change_name_and_icon_test.dart'
as change_workspace_name_and_icon_test;
import 'workspace/collaborative_workspace_test.dart'
as collaboration_workspace_test;
import 'workspace/workspace_settings_test.dart' as workspace_settings_test;
Future<void> main() async {
preset_af_cloud_env_test.main();
@@ -16,4 +19,11 @@ Future<void> main() async {
// workspace
collaboration_workspace_test.main();
change_workspace_name_and_icon_test.main();
workspace_settings_test.main();
// document
document_drag_block_test.main();
// sidebar
sidebar_move_page_test.main();
}
@@ -0,0 +1,60 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/constants.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document delete block: ', () {
testWidgets('hover on the block and delete it', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before delete
final path = [1];
final beforeDeletedBlock = tester.editor.getNodeAtPath(path);
// hover on the block and delete it
final optionButton = find.byWidgetPredicate(
(widget) =>
widget is DraggableOptionButton &&
widget.blockComponentContext.node.path.equals(path),
);
await tester.hoverOnWidget(
optionButton,
onHover: () async {
// click the delete button
await tester.tapButton(optionButton);
},
);
await tester.pumpAndSettle(Durations.short1);
// click the delete button
final deleteButton =
find.findTextInFlowyText(LocaleKeys.button_delete.tr());
await tester.tapButton(deleteButton);
// wait for the deletion
await tester.pumpAndSettle(Durations.short1);
// check if the block is deleted
final afterDeletedBlock = tester.editor.getNodeAtPath([1]);
expect(afterDeletedBlock.id, isNot(equals(beforeDeletedBlock.id)));
});
});
}
@@ -0,0 +1,67 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/constants.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document drag block: ', () {
testWidgets('drag block to the top', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before move
final beforeMoveBlock = tester.editor.getNodeAtPath([1]);
// move the desktop guide to the top, above the getting started
await tester.editor.dragBlock(
[1],
const Offset(20, -80),
);
// wait for the move animation to complete
await tester.pumpAndSettle(Durations.short1);
// check if the block is moved to the top
final afterMoveBlock = tester.editor.getNodeAtPath([0]);
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
testWidgets('drag block to other block\'s child', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before move
final beforeMoveBlock = tester.editor.getNodeAtPath([10]);
// move the checkbox to the child of the block at path [9]
await tester.editor.dragBlock(
[10],
const Offset(80, -30),
);
// wait for the move animation to complete
await tester.pumpAndSettle(Durations.short1);
// check if the block is moved to the child of the block at path [9]
final afterMoveBlock = tester.editor.getNodeAtPath([9, 0]);
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
});
}
@@ -0,0 +1,121 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:universal_platform/universal_platform.dart';
import '../../shared/constants.dart';
import '../../shared/database_test_op.dart';
import '../../shared/dir.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('sidebar move page: ', () {
testWidgets('create a new document and move it to Getting started',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// click the ... button and move to Getting started
await tester.hoverOnPageName(
pageName,
onHover: () async {
await tester.tapPageOptionButton();
await tester.tapButtonWithName(
LocaleKeys.disclosureAction_moveTo.tr(),
);
},
);
// expect to see two pages
// one is in the sidebar, the other is in the move to page list
// 1. Getting started
// 2. To-dos
final gettingStarted = find.findTextInFlowyText(
Constants.gettingStartedPageName,
);
final toDos = find.findTextInFlowyText(Constants.toDosPageName);
await tester.pumpUntilFound(gettingStarted);
await tester.pumpUntilFound(toDos);
expect(gettingStarted, findsNWidgets(2));
// skip the length check on Linux temporarily,
// because it failed in expect check but the previous pumpUntilFound is successful
if (!UniversalPlatform.isLinux) {
expect(toDos, findsNWidgets(2));
// hover on the todos page, and will see a forbidden icon
await tester.hoverOnWidget(
toDos.last,
onHover: () async {
final tooltips = find.byTooltip(
LocaleKeys.space_cannotMovePageToDatabase.tr(),
);
expect(tooltips, findsOneWidget);
},
);
await tester.pumpAndSettle();
}
// move the current page to Getting started
await tester.tapButton(
gettingStarted.last,
);
await tester.pumpAndSettle();
// after moving, expect to not see the page name in the sidebar
final page = tester.findPageName(pageName);
expect(page, findsNothing);
// click to expand the getting started page
await tester.expandOrCollapsePage(
pageName: Constants.gettingStartedPageName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
// expect to see the page name in the getting started page
final pageInGettingStarted = tester.findPageName(
pageName,
parentName: Constants.gettingStartedPageName,
);
expect(pageInGettingStarted, findsOneWidget);
});
});
}
@@ -32,69 +32,66 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// final email = '${uuid()}@appflowy.io';
group('collaborative workspace', () {
group('collaborative workspace: ', () {
// combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it',
(tester) async {
// only run the test when the feature flag is on
// if (!FeatureFlag.collaborativeWorkspace.isOn) {
// return;
// }
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
// await tester.initializeAppFlowy(
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
// email: email,
// );
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// const name = 'AppFlowy.IO';
// // the workspace will be opened after created
// await tester.createCollaborativeWorkspace(name);
const name = 'AppFlowy.IO';
// the workspace will be opened after created
await tester.createCollaborativeWorkspace(name);
// final loading = find.byType(Loading);
// await tester.pumpUntilNotFound(loading);
final loading = find.byType(Loading);
await tester.pumpUntilNotFound(loading);
// Finder success;
Finder success;
// final Finder items = find.byType(WorkspaceMenuItem);
final Finder items = find.byType(WorkspaceMenuItem);
// // delete the newly created workspace
// await tester.openCollaborativeWorkspaceMenu();
// await tester.pumpUntilFound(items);
// delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu();
await tester.pumpUntilFound(items);
// expect(items, findsNWidgets(2));
// expect(
// tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
// name,
// );
expect(items, findsNWidgets(2));
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
name,
);
// final secondWorkspace = find.byType(WorkspaceMenuItem).last;
// await tester.hoverOnWidget(
// secondWorkspace,
// onHover: () async {
// // click the more button
// final moreButton = find.byType(WorkspaceMoreActionList);
// expect(moreButton, findsOneWidget);
// await tester.tapButton(moreButton);
// // click the delete button
// final deleteButton = find.text(LocaleKeys.button_delete.tr());
// expect(deleteButton, findsOneWidget);
// await tester.tapButton(deleteButton);
// // see the delete confirm dialog
// final confirm =
// find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
// expect(confirm, findsOneWidget);
// await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// // delete success
// success = find.text(LocaleKeys.workspace_createSuccess.tr());
// await tester.pumpUntilFound(success);
// expect(success, findsOneWidget);
// await tester.pumpUntilNotFound(success);
// },
// );
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
await tester.hoverOnWidget(
secondWorkspace,
onHover: () async {
// click the more button
final moreButton = find.byType(WorkspaceMoreActionList);
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
// click the delete button
final deleteButton = find.text(LocaleKeys.button_delete.tr());
expect(deleteButton, findsOneWidget);
await tester.tapButton(deleteButton);
// see the delete confirm dialog
final confirm =
find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
expect(confirm, findsOneWidget);
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// delete success
success = find.text(LocaleKeys.workspace_createSuccess.tr());
await tester.pumpUntilFound(success);
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
},
);
});
});
}
@@ -0,0 +1,96 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/database_test_op.dart';
import '../../shared/dir.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('workspace settings: ', () {
testWidgets(
'change document width',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.workspace);
final documentWidthSettings = find.findTextInFlowyText(
LocaleKeys.settings_appearance_documentSettings_width.tr(),
);
final scrollable = find.ancestor(
of: find.byType(SettingsWorkspaceView),
matching: find.descendant(
of: find.byType(SingleChildScrollView),
matching: find.byType(Scrollable),
),
);
await tester.scrollUntilVisible(
documentWidthSettings,
0,
scrollable: scrollable,
);
await tester.pumpAndSettle();
// change the document width
final slider = find.byType(Slider);
final oldValue = tester.widget<Slider>(slider).value;
await tester.drag(slider, const Offset(-100, 0));
await tester.pumpAndSettle();
// check the document width is changed
expect(tester.widget<Slider>(slider).value, lessThan(oldValue));
// click the reset button
final resetButton = find.descendant(
of: find.byType(DocumentPaddingSetting),
matching: find.byType(SettingsResetButton),
);
await tester.tap(resetButton);
await tester.pumpAndSettle();
// check the document width is reset
expect(
tester.widget<Slider>(slider).value,
EditorStyleCustomizer.maxDocumentWidth,
);
},
);
});
}
@@ -25,6 +25,7 @@ void main() {
name: fieldName,
layout: ViewLayoutPB.Board,
);
await tester.dismissRowDetailPage();
await tester.tapButton(card1);
await tester.changeFieldTypeOfFieldWithName(
fieldName,
@@ -1,17 +1,21 @@
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board group test', () {
group('board group test:', () {
testWidgets('move row to another group', (tester) async {
const card1Name = 'Card 1';
await tester.initializeAppFlowy();
@@ -46,5 +50,105 @@ void main() {
final card1StatusText = tester.widget<Text>(card1StatusFinder).data;
expect(card1StatusText, 'Doing');
});
testWidgets('rename group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final headers = find.byType(BoardColumnHeader);
expect(headers, findsNWidgets(4));
// try to tap no status
final noStatus = headers.first;
expect(
find.descendant(of: noStatus, matching: find.text("No Status")),
findsOneWidget,
);
await tester.tapButton(noStatus);
expect(
find.descendant(of: noStatus, matching: find.byType(TextField)),
findsNothing,
);
// tap on To Do and edit it
final todo = headers.at(1);
expect(
find.descendant(of: todo, matching: find.text("To Do")),
findsOneWidget,
);
await tester.tapButton(todo);
await tester.enterText(
find.descendant(of: todo, matching: find.byType(TextField)),
"tada",
);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
final newHeaders = find.byType(BoardColumnHeader);
expect(newHeaders, findsNWidgets(4));
final tada = find.byType(BoardColumnHeader).at(1);
expect(
find.descendant(of: tada, matching: find.byType(TextField)),
findsNothing,
);
expect(
find.descendant(
of: tada,
matching: find.text("tada"),
),
findsOneWidget,
);
});
testWidgets('edit select option from row detail', (tester) async {
const card1Name = 'Card 1';
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
await tester.tapButton(
find.descendant(
of: find.byType(RowCard),
matching: find.text(card1Name),
),
);
await tester.tapGridFieldWithNameInRowDetailPage("Status");
await tester.tapButton(
find.byWidgetPredicate(
(widget) =>
widget is SelectOptionTagCell && widget.option.name == "To Do",
),
);
final editor = find.byType(SelectOptionEditor);
await tester.enterText(
find.descendant(of: editor, matching: find.byType(TextField)),
"tada",
);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.dismissFieldEditor();
await tester.dismissRowDetailPage();
final newHeaders = find.byType(BoardColumnHeader);
expect(newHeaders, findsNWidgets(4));
final tada = find.byType(BoardColumnHeader).at(1);
expect(
find.descendant(of: tada, matching: find.byType(TextField)),
findsNothing,
);
expect(
find.descendant(
of: tada,
matching: find.text("tada"),
),
findsOneWidget,
);
});
});
}
@@ -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/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
@@ -77,47 +78,47 @@ void main() {
shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length;
expect(shownGroups, 4);
});
});
testWidgets('delete a group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
testWidgets('delete a group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
// tap group option button for the first group. Delete shouldn't show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.first,
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
// tap group option button for the first group. Delete shouldn't show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.first,
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
// dismiss the popup
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// dismiss the popup
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// tap group option button for the first group. Delete should show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.at(1),
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
// tap group option button for the first group. Delete should show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.at(1),
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
// Tap the delete button and confirm
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
await tester.tapDialogOkButton();
// Tap the delete button and confirm
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
await tester.tapButtonWithName(LocaleKeys.space_delete.tr());
// Expect number of groups to decrease by one
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
// Expect number of groups to decrease by one
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
});
});
}
@@ -29,7 +29,7 @@ void main() {
},
);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.tapOKButton();
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
expect(find.text(name), findsNothing);
});
@@ -51,6 +51,37 @@ void main() {
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
});
testWidgets('duplicate item in ToDo card then delete', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
const name = 'Card 1';
final card1 = find.text(name);
await tester.hoverOnWidget(
card1,
onHover: () async {
final moreOption = find.byType(MoreCardOptionsAccessory);
await tester.tapButton(moreOption);
},
);
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
// get the last widget that contains the name
final duplicatedCard = find.textContaining(name, findRichText: true).last;
await tester.hoverOnWidget(
duplicatedCard,
onHover: () async {
final moreOption = find.byType(MoreCardOptionsAccessory);
await tester.tapButton(moreOption);
},
);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
expect(find.textContaining(name, findRichText: true), findsNWidgets(1));
});
testWidgets('add new group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
import 'board_add_row_test.dart' as board_add_row_test;
import 'board_group_test.dart' as board_group_test;
import 'board_row_test.dart' as board_row_test;
import 'board_field_test.dart' as board_field_test;
import 'board_hide_groups_test.dart' as board_hide_groups_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -11,4 +13,6 @@ void main() {
board_row_test.main();
board_add_row_test.main();
board_group_test.main();
board_field_test.main();
board_hide_groups_test.main();
}
@@ -11,7 +11,7 @@ void main() {
group('grid filter:', () {
testWidgets('add text filter', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a filter
await tester.tapDatabaseFilterButton();
@@ -19,68 +19,68 @@ void main() {
await tester.tapFilterButtonInGrid('Name');
// enter 'A' in the filter text field
await tester.assertNumberOfRowsInGridPage(10);
tester.assertNumberOfRowsInGridPage(10);
await tester.enterTextInTextFilter('A');
await tester.assertNumberOfRowsInGridPage(1);
tester.assertNumberOfRowsInGridPage(1);
// after remove the filter, the grid should show all rows
await tester.enterTextInTextFilter('');
await tester.assertNumberOfRowsInGridPage(10);
tester.assertNumberOfRowsInGridPage(10);
await tester.enterTextInTextFilter('B');
await tester.assertNumberOfRowsInGridPage(1);
tester.assertNumberOfRowsInGridPage(1);
// open the menu to delete the filter
await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
await tester.assertNumberOfRowsInGridPage(10);
tester.assertNumberOfRowsInGridPage(10);
await tester.pumpAndSettle();
});
testWidgets('add checkbox filter', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');
await tester.assertNumberOfRowsInGridPage(5);
tester.assertNumberOfRowsInGridPage(5);
await tester.tapFilterButtonInGrid('Done');
await tester.tapCheckboxFilterButtonInGrid();
await tester.tapUnCheckedButtonOnCheckboxFilter();
await tester.assertNumberOfRowsInGridPage(5);
tester.assertNumberOfRowsInGridPage(5);
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
await tester.assertNumberOfRowsInGridPage(10);
tester.assertNumberOfRowsInGridPage(10);
await tester.pumpAndSettle();
});
testWidgets('add checklist filter', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');
// By default, the condition of checklist filter is 'uncompleted'
await tester.assertNumberOfRowsInGridPage(9);
tester.assertNumberOfRowsInGridPage(9);
await tester.tapFilterButtonInGrid('checklist');
await tester.tapChecklistFilterButtonInGrid();
await tester.tapCompletedButtonOnChecklistFilter();
await tester.assertNumberOfRowsInGridPage(1);
tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
testWidgets('add single select filter', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a filter
await tester.tapDatabaseFilterButton();
@@ -90,27 +90,27 @@ void main() {
// select the option 's6'
await tester.tapOptionFilterWithName('s6');
await tester.assertNumberOfRowsInGridPage(0);
tester.assertNumberOfRowsInGridPage(0);
// unselect the option 's6'
await tester.tapOptionFilterWithName('s6');
await tester.assertNumberOfRowsInGridPage(10);
tester.assertNumberOfRowsInGridPage(10);
// select the option 's5'
await tester.tapOptionFilterWithName('s5');
await tester.assertNumberOfRowsInGridPage(1);
tester.assertNumberOfRowsInGridPage(1);
// select the option 's4'
await tester.tapOptionFilterWithName('s4');
// The row with 's4' should be shown.
await tester.assertNumberOfRowsInGridPage(1);
tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
testWidgets('add multi select filter', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a filter
await tester.tapDatabaseFilterButton();
@@ -124,17 +124,17 @@ void main() {
// select the option 'm1'. Any option with 'm1' should be shown.
await tester.tapOptionFilterWithName('m1');
await tester.assertNumberOfRowsInGridPage(5);
tester.assertNumberOfRowsInGridPage(5);
await tester.tapOptionFilterWithName('m1');
// select the option 'm2'. Any option with 'm2' should be shown.
await tester.tapOptionFilterWithName('m2');
await tester.assertNumberOfRowsInGridPage(4);
tester.assertNumberOfRowsInGridPage(4);
await tester.tapOptionFilterWithName('m2');
// select the option 'm4'. Any option with 'm4' should be shown.
await tester.tapOptionFilterWithName('m4');
await tester.assertNumberOfRowsInGridPage(1);
tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
@@ -4,9 +4,12 @@ import 'package:flutter/services.dart';
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/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -18,12 +21,13 @@ import 'package:path_provider/path_provider.dart';
import '../../shared/database_test_op.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('media type option in database', () {
testWidgets('add media field and add files', (tester) async {
testWidgets('add media field and add files two times', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -92,5 +96,217 @@ void main() {
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('add two files at once', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Prepare files for upload from local
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('delete files', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Prepare files for upload from local
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
// Tap on the three dots menu for the first RenderMedia
final mediaMenuFinder = find.descendant(
of: find.byType(RenderMedia),
matching: find.byFlowySvg(FlowySvgs.three_dots_s),
);
await tester.tap(mediaMenuFinder.first);
await tester.pumpAndSettle();
// Tap on the delete button
await tester.tap(find.text(LocaleKeys.grid_media_delete.tr()));
await tester.pumpAndSettle();
// Tap on Delete button in the confirmation dialog
await tester.tap(
find.descendant(
of: find.byType(SpaceCancelOrConfirmButton),
matching: find.text(LocaleKeys.grid_media_delete.tr()),
),
);
await tester.pumpAndSettle(const Duration(seconds: 1));
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('hide file names', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Prepare files for upload from local
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
await tester.dismissCellEditor();
await tester.pumpAndSettle();
// Open first row in row detail view then toggle hide file names
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to be shown
expect(find.text('sample.jpeg'), findsOneWidget);
expect(find.text('sample.gif'), findsOneWidget);
await tester.tapGridFieldWithNameInRowDetailPage('Type');
await tester.pumpAndSettle();
// Toggle hide file names
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
await tester.dismissRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to be hidden
expect(find.text('sample.jpeg'), findsNothing);
expect(find.text('sample.gif'), findsNothing);
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
});
}
@@ -0,0 +1,191 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/database_test_op.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database row cover', () {
testWidgets('add image to media field and check if cover is set (grid)',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Prepare file for upload from local
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(paths: [imagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
// Close cell editor
await tester.dismissCellEditor();
// Open first row in row detail view
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect a cover to be shown
expect(find.byType(RowCover), findsOneWidget);
// Remove the temp file
await Future.wait([file.delete()]);
});
testWidgets('add and remove cover from Row Detail Card', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Open first row in row detail view
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect no cover
expect(find.byType(RowCover), findsNothing);
// Hover on RowBanner to show Add Cover button
await tester.hoverRowBanner();
// Click on Add Cover button
await tester.tapAddCoverButton();
// Expect a cover to be shown - the default asset cover
expect(find.byType(RowCover), findsOneWidget);
// Tap on the delete cover button
await tester.tapButton(find.byType(DeleteCoverButton));
await tester.pumpAndSettle();
// Expect no cover to be shown
expect(find.byType(AFImage), findsNothing);
});
testWidgets('add and change cover and check in Board', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
await tester.pumpAndSettle();
// Open "Card 1"
await tester.tap(find.text('Card 1'), warnIfMissed: false);
await tester.pumpAndSettle();
// Expect no cover
expect(find.byType(RowCover), findsNothing);
// Hover on RowBanner to show Add Cover button
await tester.hoverRowBanner();
// Click on Add Cover button
await tester.tapAddCoverButton();
// Expect default cover to be shown
expect(find.byType(RowCover), findsOneWidget);
// Prepare image for upload from local
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(paths: [imagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Hover on RowBanner to show Change Cover button
await tester.hoverRowBanner();
// Tap on the change cover button
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
await tester.pumpAndSettle();
// Change tab to Upload tab
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_label.tr(),
);
// Tab on the upload button
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
// Expect one cover
expect(find.byType(RowCover), findsOneWidget);
// Expect the cover to be shown both in RowCover and in CardCover
expect(find.byType(AFImage), findsNWidgets(2));
// Dismiss Row Detail Page
await tester.dismissRowDetailPage();
// Expect a cover to be shown in CardCover
expect(
find.descendant(
of: find.byType(CardCover),
matching: find.byType(AFImage),
),
findsOneWidget,
);
// Remove the temp file
await Future.wait([file.delete()]);
});
});
}
@@ -1,12 +1,14 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -36,26 +38,7 @@ void main() {
await tester.assertDocumentExistInRowDetailPage();
});
testWidgets('add emoji', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// Create a new grid
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.tapEmoji('😀');
// After select the emoji, the EmojiButton will show up
await tester.tapButton(find.byType(EmojiButton));
});
testWidgets('update emoji', (tester) async {
testWidgets('add and update emoji', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -68,8 +51,18 @@ void main() {
await tester.openEmojiPicker();
await tester.tapEmoji('😀');
// Update existing selected emoji
await tester.tapButton(find.byType(EmojiButton));
// expect to find the emoji selected
final firstEmojiFinder = find.byWidgetPredicate(
(w) => w is FlowyText && w.text == '😀',
);
// There are 2 eomjis - one in the row banner and another in the primary cell
expect(firstEmojiFinder, findsNWidgets(2));
// Update existing selected emoji - tap on it to update
await tester.tapButton(find.byType(EmojiIconWidget));
await tester.pumpAndSettle();
await tester.tapEmoji('😅');
// The emoji already displayed in the row banner
@@ -96,7 +89,9 @@ void main() {
await tester.tapEmoji('😀');
// Remove the emoji
await tester.tapButton(find.byType(RemoveEmojiButton));
await tester.tapButton(find.byType(EmojiIconWidget));
await tester.tapButton(find.text(LocaleKeys.button_remove.tr()));
final emojiText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == '😀',
);
@@ -321,7 +316,7 @@ void main() {
await tester.tapRowDetailPageDeleteRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(2);
tester.assertNumberOfRowsInGridPage(2);
});
testWidgets('duplicate row', (tester) async {
@@ -338,7 +333,7 @@ void main() {
await tester.tapRowDetailPageDuplicateRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(4);
tester.assertNumberOfRowsInGridPage(4);
});
});
}
@@ -1,66 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('create row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateRowButtonInGrid();
// 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4);
await tester.pumpAndSettle();
});
testWidgets('create row from row menu of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonInRowMenuOfGrid();
// 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4);
await tester.pumpAndSettle();
});
testWidgets('delete row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.pumpAndSettle();
await tester.tapDeleteOnRowMenu();
await tester.pumpAndSettle();
// 3 initial rows - 1 deleted
await tester.assertNumberOfRowsInGridPage(2);
});
});
testWidgets('check number of row indicator in the initial grid',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.pumpAndSettle();
});
});
}
@@ -9,7 +9,7 @@ void main() {
group('database', () {
testWidgets('import v0.2.0 database data', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// wait the database data is loaded
await tester.pumpAndSettle(const Duration(microseconds: 500));
@@ -9,7 +9,7 @@ void main() {
group('grid', () {
testWidgets('add text sort', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
@@ -60,7 +60,7 @@ void main() {
// delete all sorts
await tester.tapSortMenuInSettingBar();
await tester.tapAllSortButton();
await tester.tapDeleteAllSortsButton();
// check the text cell order
for (final (index, content) in <String>[
@@ -85,7 +85,7 @@ void main() {
});
testWidgets('add checkbox sort', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@@ -135,7 +135,7 @@ void main() {
});
testWidgets('add number sort', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
@@ -187,7 +187,7 @@ void main() {
});
testWidgets('add checkbox and number sort', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@@ -266,7 +266,7 @@ void main() {
});
testWidgets('reorder sort', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@@ -207,7 +207,7 @@ void main() {
final image = await rootBundle.load('assets/test/images/sample.png');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('png', bytes), (editorState) {
expect(editorState.document.root.children.length, 2);
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
@@ -218,7 +218,7 @@ void main() {
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
expect(editorState.document.root.children.length, 2);
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
@@ -280,7 +280,7 @@ void main() {
html: html,
image: ('png', bytes),
(editorState) {
expect(editorState.document.root.children.length, 2);
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
},
@@ -313,6 +313,7 @@ void main() {
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) {
// the second one is the paragraph node
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -0,0 +1,203 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid create row test:', () {
testWidgets('from the bottom', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
final expected = tester.getGridRows();
// create row
await tester.tapCreateRowButtonInGrid();
final actual = tester.getGridRows();
expect(actual.slice(0, 3), orderedEquals(expected));
expect(actual.length, equals(4));
tester.assertNumberOfRowsInGridPage(4);
});
testWidgets('from a row\'s menu', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
final expected = tester.getGridRows();
// create row
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
final actual = tester.getGridRows();
expect([actual[0], actual[2], actual[3]], orderedEquals(expected));
expect(actual.length, equals(4));
tester.assertNumberOfRowsInGridPage(4);
});
testWidgets('with sort configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final unsorted = tester.getGridRows();
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
final sorted = [
unsorted[7],
unsorted[8],
unsorted[1],
unsorted[9],
unsorted[11],
unsorted[10],
unsorted[6],
unsorted[12],
unsorted[2],
unsorted[0],
unsorted[3],
unsorted[5],
unsorted[4],
];
List actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// create row
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
// cancel
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_cancel.tr());
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// try again, but confirm this time
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_remove.tr());
// verify grid data
actual = tester.getGridRows();
expect(actual.length, equals(14));
tester.assertNumberOfRowsInGridPage(14);
});
testWidgets('with filter configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
final filtered = [
original[1],
original[3],
original[5],
original[6],
original[7],
original[9],
original[12],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
// create row (one before and after the first row, and one at the bottom)
await tester.tapCreateRowButtonInGrid();
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
await tester.hoverOnFirstRowOfGrid(() async {
await tester.tapRowMenuButtonInGrid();
await tester.tapCreateRowAboveButtonInRowMenu();
});
actual = tester.getGridRows();
expect(actual.length, equals(10));
tester.assertNumberOfRowsInGridPage(10);
actual = [
actual[1],
actual[3],
actual[4],
actual[5],
actual[6],
actual[7],
actual[8],
];
expect(actual, orderedEquals(filtered));
// delete the filter
await tester.tapFilterButtonInGrid('Registration Complete');
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
// verify grid data
actual = tester.getGridRows();
expect(actual.length, equals(16));
tester.assertNumberOfRowsInGridPage(16);
actual = [
actual[0],
actual[2],
actual[4],
actual[5],
actual[6],
actual[7],
actual[8],
actual[9],
actual[10],
actual[11],
actual[12],
actual[13],
actual[14],
];
expect(actual, orderedEquals(original));
});
// TODO(RS): move to somewhere else
testWidgets('delete row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.pumpAndSettle();
await tester.tapDeleteOnRowMenu();
await tester.pumpAndSettle();
// 3 initial rows - 1 deleted
tester.assertNumberOfRowsInGridPage(2);
});
});
});
}
@@ -0,0 +1,120 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid simultaneous sort and filter test:', () {
// testWidgets('delete filter with active sort', (tester) async {
// await tester.openTestDatabase(v069GridFileName);
// // get grid data
// final original = tester.getGridRows();
// // add a filter
// await tester.tapDatabaseFilterButton();
// await tester.tapCreateFilterByFieldType(
// FieldType.Checkbox,
// 'Registration Complete',
// );
// // add a sort
// await tester.tapDatabaseSortButton();
// await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
// final filteredAndSorted = [
// original[7],
// original[1],
// original[9],
// original[6],
// original[12],
// original[3],
// original[5],
// ];
// // verify grid data
// List actual = tester.getGridRows();
// expect(actual, orderedEquals(filteredAndSorted));
// // delete the filter
// await tester.tapFilterButtonInGrid('Registration Complete');
// await tester
// .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
// await tester.tapDeleteFilterButtonInGrid();
// final sorted = [
// original[7],
// original[8],
// original[1],
// original[9],
// original[11],
// original[10],
// original[6],
// original[12],
// original[2],
// original[0],
// original[3],
// original[5],
// original[4],
// ];
// // verify grid data
// actual = tester.getGridRows();
// expect(actual, orderedEquals(sorted));
// });
testWidgets('delete sort with active fiilter', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
final filteredAndSorted = [
original[7],
original[1],
original[9],
original[6],
original[12],
original[3],
original[5],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filteredAndSorted));
// delete the sort
await tester.tapSortMenuInSettingBar();
await tester.tapDeleteAllSortsButton();
final filtered = [
original[1],
original[3],
original[5],
original[6],
original[7],
original[9],
original[12],
];
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
});
});
}
@@ -0,0 +1,183 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid reopen test:', () {
testWidgets('base case', (tester) async {
await tester.openTestDatabase(v069GridFileName);
final expected = tester.getGridRows();
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
final actual = tester.getGridRows();
expect(actual, orderedEquals(expected));
});
testWidgets('with sort configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final unsorted = tester.getGridRows();
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
final sorted = [
unsorted[7],
unsorted[8],
unsorted[1],
unsorted[9],
unsorted[11],
unsorted[10],
unsorted[6],
unsorted[12],
unsorted[2],
unsorted[0],
unsorted[3],
unsorted[5],
unsorted[4],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// delete sorts
// TODO(RS): Shouldn't the sort/filter list show automatically!?
await tester.tapDatabaseSortButton();
await tester.tapSortMenuInSettingBar();
await tester.tapDeleteAllSortsButton();
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unsorted));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unsorted));
});
testWidgets('with filter configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final unfiltered = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
final filtered = [
unfiltered[1],
unfiltered[3],
unfiltered[5],
unfiltered[6],
unfiltered[7],
unfiltered[9],
unfiltered[12],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
// delete the filter
// TODO(RS): Shouldn't the sort/filter list show automatically!?
await tester.tapDatabaseFilterButton();
await tester.tapFilterButtonInGrid('Registration Complete');
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unfiltered));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unfiltered));
});
testWidgets('with both filter and sort configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
final filteredAndSorted = [
original[7],
original[1],
original[9],
original[6],
original[12],
original[3],
original[5],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filteredAndSorted));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(filteredAndSorted));
});
});
}
@@ -0,0 +1,214 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid reorder row test:', () {
testWidgets('base case', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// reorder row
await tester.reorderRow(original[4], original[1]);
// verify grid data
List reordered = [
original[0],
original[4],
original[1],
original[2],
original[3],
original[5],
original[6],
original[7],
original[8],
original[9],
original[10],
original[11],
original[12],
];
List actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// reorder row
await tester.reorderRow(reordered[1], reordered[3]);
// verify grid data
reordered = [
original[0],
original[1],
original[2],
original[4],
original[3],
original[5],
original[6],
original[7],
original[8],
original[9],
original[10],
original[11],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// reorder row
await tester.reorderRow(reordered[2], reordered[0]);
// verify grid data
reordered = [
original[2],
original[0],
original[1],
original[4],
original[3],
original[5],
original[6],
original[7],
original[8],
original[9],
original[10],
original[11],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
});
testWidgets('with active sort', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
// verify grid data
final sorted = [
original[7],
original[8],
original[1],
original[9],
original[11],
original[10],
original[6],
original[12],
original[2],
original[0],
original[3],
original[5],
original[4],
];
List actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// reorder row
await tester.reorderRow(original[4], original[1]);
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_cancel.tr());
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
});
testWidgets('with active filter', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
final filtered = [
original[1],
original[3],
original[5],
original[6],
original[7],
original[9],
original[12],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
// reorder row
await tester.reorderRow(filtered[3], filtered[1]);
// verify grid data
List reordered = [
original[1],
original[6],
original[3],
original[5],
original[7],
original[9],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// reorder row
await tester.reorderRow(reordered[3], reordered[5]);
// verify grid data
reordered = [
original[1],
original[6],
original[3],
original[7],
original[9],
original[5],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// delete the filter
await tester.tapFilterButtonInGrid('Registration Complete');
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
// verify grid data
final expected = [
original[0],
original[1],
original[2],
original[6],
original[3],
original[4],
original[7],
original[8],
original[9],
original[5],
original[10],
original[11],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(expected));
});
});
}
@@ -0,0 +1,13 @@
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:flutter_test/flutter_test.dart';
extension GridTestExtensions on WidgetTester {
List<RowId> getGridRows() {
final databaseController =
widget<GridPage>(find.byType(GridPage)).databaseController;
return [
...databaseController.rowCache.rowInfos.map((e) => e.rowId),
];
}
}
@@ -15,7 +15,7 @@ import 'package:integration_test/integration_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/editor_test_operations.dart';
import '../../shared/document_test_operations.dart';
import '../../shared/expectation.dart';
import '../../shared/keyboard.dart';
@@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart';
import 'notifications_settings_test.dart' as notifications_settings_test;
import 'settings_billing_test.dart' as settings_billing_test;
import 'shortcuts_settings_test.dart' as shortcuts_settings_test;
import 'sign_in_page_settings_test.dart' as sign_in_page_settings_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -10,4 +11,5 @@ void main() {
notifications_settings_test.main();
settings_billing_test.main();
shortcuts_settings_test.main();
sign_in_page_settings_test.main();
}
@@ -0,0 +1,103 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:toastification/toastification.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Finder findServerType(AuthenticatorType type) {
return find
.descendant(
of: find.byType(SettingsServerDropdownMenu),
matching: find.findTextInFlowyText(
type.label,
),
)
.last;
}
group('sign-in page settings: ', () {
testWidgets('change server type', (tester) async {
await tester.initializeAppFlowy();
// reset the app to the default state
await useAppFlowyBetaCloudWithURL(
kAppflowyCloudUrl,
AuthenticatorType.appflowyCloud,
);
// open the settings page
final settingsButton = find.byType(DesktopSignInSettingsButton);
await tester.tapButton(settingsButton);
expect(find.byType(SimpleSettingsDialog), findsOneWidget);
// the default type should be appflowy cloud
final appflowyCloudType = findServerType(AuthenticatorType.appflowyCloud);
expect(appflowyCloudType, findsOneWidget);
// change the server type to self-host
await tester.tapButton(appflowyCloudType);
final selfhostedButton = findServerType(
AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapButton(selfhostedButton);
// update server url
const serverUrl = 'https://test.appflowy.cloud';
await tester.enterText(
find.byKey(kSelfHostedTextInputFieldKey),
serverUrl,
);
await tester.pumpAndSettle();
await tester.tapButton(
find.findTextInFlowyText(LocaleKeys.button_save.tr()),
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// open settings page to check the result
await tester.tapButton(settingsButton);
// check the server type
expect(
findServerType(AuthenticatorType.appflowyCloudSelfHost),
findsOneWidget,
);
// check the server url
expect(
find.text(serverUrl),
findsOneWidget,
);
// reset to appflowy cloud
await tester.tapButton(
findServerType(AuthenticatorType.appflowyCloudSelfHost),
);
// change the server type to appflowy cloud
await tester.tapButton(
findServerType(AuthenticatorType.appflowyCloud),
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// check the server type
await tester.tapButton(settingsButton);
expect(
findServerType(AuthenticatorType.appflowyCloud),
findsOneWidget,
);
});
});
}
@@ -0,0 +1,122 @@
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:integration_test/integration_test.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Zoom in/out: ', () {
Future<void> resetAppFlowyScaleFactor(
WindowSizeManager windowSizeManager,
) async {
appflowyScaleFactor = 1.0;
await windowSizeManager.setScaleFactor(1.0);
}
testWidgets('Zoom in', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
double currentScaleFactor = 1.0;
// this value can't be defined in the setUp method, because the windowSizeManager is not initialized yet.
final windowSizeManager = WindowSizeManager();
await resetAppFlowyScaleFactor(windowSizeManager);
// zoom in 2 times
for (final keycode in zoomInKeyCodes) {
if (UniversalPlatform.isLinux &&
keycode.logicalKey == LogicalKeyboardKey.add) {
// Key LogicalKeyboardKey#79c9b(keyId: "0x0000002b", keyLabel: "+", debugName: "Add") not found in
// linux keyCode map
continue;
}
// test each keycode 2 times
for (var i = 0; i < 2; i++) {
await tester.simulateKeyEvent(
keycode.logicalKey,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
// Register the physical key for the "Add" key, otherwise the test will fail and throw an error:
// Physical key for LogicalKeyboardKey#79c9b(keyId: "0x0000002b", keyLabel: "+", debugName: "Add")
// not found in known physical keys
physicalKey: keycode.logicalKey == LogicalKeyboardKey.add
? PhysicalKeyboardKey.equal
: null,
);
await tester.pumpAndSettle();
currentScaleFactor += 0.1;
final scaleFactor = await windowSizeManager.getScaleFactor();
expect(currentScaleFactor, appflowyScaleFactor);
expect(currentScaleFactor, scaleFactor);
}
}
});
testWidgets('Reset zoom', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final windowSizeManager = WindowSizeManager();
for (final keycode in resetZoomKeyCodes) {
await tester.simulateKeyEvent(
keycode.logicalKey,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
final scaleFactor = await windowSizeManager.getScaleFactor();
expect(1.0, appflowyScaleFactor);
expect(1.0, scaleFactor);
}
});
testWidgets('Zoom out', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
double currentScaleFactor = 1.0;
final windowSizeManager = WindowSizeManager();
await resetAppFlowyScaleFactor(windowSizeManager);
// zoom out 2 times
for (final keycode in zoomOutKeyCodes) {
if (UniversalPlatform.isLinux &&
keycode.logicalKey == LogicalKeyboardKey.numpadSubtract) {
// Key LogicalKeyboardKey#2c39f(keyId: "0x20000022d", keyLabel: "Numpad Subtract", debugName: "Numpad
// Subtract") not found in linux keyCode map
continue;
}
// test each keycode 2 times
for (var i = 0; i < 2; i++) {
await tester.simulateKeyEvent(
keycode.logicalKey,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
currentScaleFactor -= 0.1;
final scaleFactor = await windowSizeManager.getScaleFactor();
expect(currentScaleFactor, appflowyScaleFactor);
expect(currentScaleFactor, scaleFactor);
}
}
});
});
}
@@ -6,8 +6,8 @@ import 'desktop/database/database_field_settings_test.dart'
as database_field_settings_test;
import 'desktop/database/database_field_test.dart' as database_field_test;
import 'desktop/database/database_filter_test.dart' as database_filter_test;
import 'desktop/database/database_media_test.dart' as database_media_test;
import 'desktop/database/database_row_page_test.dart' as database_row_page_test;
import 'desktop/database/database_row_test.dart' as database_row_test;
import 'desktop/database/database_setting_test.dart' as database_setting_test;
import 'desktop/database/database_share_test.dart' as database_share_test;
import 'desktop/database/database_sort_test.dart' as database_sort_test;
@@ -29,12 +29,12 @@ Future<void> runIntegration2OnDesktop() async {
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_row_test.main();
database_setting_test.main();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
database_media_test.main();
// DON'T add more tests here. This is the second test runner for desktop.
}
@@ -1,6 +1,14 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/database/database_row_cover_test.dart'
as database_row_cover_test;
import 'desktop/grid/grid_create_row_test.dart' as grid_create_row_test_runner;
import 'desktop/grid/grid_filter_and_sort_test.dart'
as grid_filter_and_sort_test_runner;
import 'desktop/grid/grid_reopen_test.dart' as grid_reopen_test_runner;
import 'desktop/grid/grid_reorder_row_test.dart'
as grid_reorder_row_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
@@ -9,6 +17,7 @@ import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
import 'desktop/uncategorized/import_files_test.dart' as import_files_test;
import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/uncategorized/zoom_in_out_test.dart' as zoom_in_out_test;
Future<void> main() async {
await runIntegration3OnDesktop();
@@ -30,4 +39,11 @@ Future<void> runIntegration3OnDesktop() async {
sidebar_test_runner.main();
board_test_runner.main();
tabs_test.main();
database_row_cover_test.main();
grid_reopen_test_runner.main();
grid_create_row_test_runner.main();
grid_reorder_row_test_runner.main();
grid_filter_and_sort_test_runner.main();
zoom_in_out_test.main();
}
@@ -1,10 +1,5 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -17,11 +12,14 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.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_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
@@ -35,6 +33,10 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'emoji.dart';
@@ -58,6 +60,14 @@ extension CommonOperations on WidgetTester {
}
}
Future<void> tapContinousAnotherWay() async {
// local version
await tapButtonWithName(LocaleKeys.signIn_continueAnotherWay.tr());
if (Platform.isWindows) {
await pumpAndSettle(const Duration(milliseconds: 200));
}
}
/// Tap the + button on the home page.
Future<void> tapAddViewButton({
String name = gettingStarted,
@@ -236,6 +246,17 @@ extension CommonOperations on WidgetTester {
await tapButton(okButton);
}
/// Expand or collapse the page.
Future<void> expandOrCollapsePage({
required String pageName,
required ViewLayoutPB layout,
}) async {
final page = findPageName(pageName, layout: layout);
await hoverOnWidget(page);
final expandButton = find.byType(ViewItemDefaultLeftIcon);
await tapButton(expandButton.first);
}
/// Tap the restore button.
///
/// the restore button will show after the current page is deleted.
@@ -317,6 +338,67 @@ extension CommonOperations on WidgetTester {
}
}
/// Create a new page in the space
Future<void> createNewPageInSpace({
required String spaceName,
required ViewLayoutPB layout,
bool openAfterCreated = true,
String? pageName,
}) async {
final currentSpace = find.byWidgetPredicate(
(widget) => widget is CurrentSpace && widget.space.name == spaceName,
);
if (currentSpace.evaluate().isEmpty) {
throw Exception('Current space not found');
}
await hoverOnWidget(
currentSpace,
onHover: () async {
// click the + button
await clickAddPageButtonInSpaceHeader();
await tapButtonWithName(layout.menuName);
},
);
await pumpAndSettle();
if (pageName != null) {
// move the cursor to other place to disable to tooltips
await tapAt(Offset.zero);
// hover on new created page and change it's name
await hoverOnPageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
onHover: () async {
await renamePage(pageName);
await pumpAndSettle();
},
);
await pumpAndSettle();
}
// open the page after created
if (openAfterCreated) {
await openPage(
// if the name is null, use the default name
pageName ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
);
await pumpAndSettle();
}
}
/// Click the + button in the space header
Future<void> clickAddPageButtonInSpaceHeader() async {
final addPageButton = find.descendant(
of: find.byType(SidebarSpaceHeader),
matching: find.byType(ViewAddButton),
);
await tapButton(addPageButton);
}
/// Create a new page on the top level
Future<void> createNewPage({
ViewLayoutPB layout = ViewLayoutPB.Document,
bool openAfterCreated = true,
@@ -330,6 +412,7 @@ extension CommonOperations on WidgetTester {
bool isShiftPressed = false,
bool isAltPressed = false,
bool isMetaPressed = false,
PhysicalKeyboardKey? physicalKey,
}) async {
if (isControlPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.control);
@@ -343,8 +426,14 @@ extension CommonOperations on WidgetTester {
if (isMetaPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.meta);
}
await simulateKeyDownEvent(key);
await simulateKeyUpEvent(key);
await simulateKeyDownEvent(
key,
physicalKey: physicalKey,
);
await simulateKeyUpEvent(
key,
physicalKey: physicalKey,
);
if (isControlPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.control);
}
@@ -572,8 +661,7 @@ extension CommonOperations on WidgetTester {
Future<void> openMoreViewActions() async {
final button = find.byType(MoreViewActions);
await tap(button);
await pumpAndSettle();
await tapButton(button);
}
/// Presses on the Duplicate ViewAction in the [MoreViewActions] popup.
@@ -581,12 +669,9 @@ extension CommonOperations on WidgetTester {
/// [openMoreViewActions] must be called beforehand!
///
Future<void> duplicateByMoreViewActions() async {
final button = find.descendant(
of: find.byType(ListView),
matching: find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewActionType.duplicate,
),
final button = find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewMoreActionType.duplicate,
);
await tap(button);
await pump();
@@ -601,7 +686,7 @@ extension CommonOperations on WidgetTester {
of: find.byType(ListView),
matching: find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewActionType.delete,
widget is ViewAction && widget.type == ViewMoreActionType.delete,
),
);
await tap(button);
@@ -0,0 +1,6 @@
class Constants {
// this page name is default page name in the new workspace
static const gettingStartedPageName = 'Getting started';
static const toDosPageName = 'To-dos';
static const generalSpaceName = 'General';
}
@@ -1,9 +1,8 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:archive/archive_io.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -60,7 +59,7 @@ class TestWorkspaceService {
final inputStream =
InputFileStream(await workspace.zip.then((value) => value.path));
final archive = ZipDecoder().decodeBuffer(inputStream);
extractArchiveToDisk(
await extractArchiveToDisk(
archive,
await TestWorkspace._parent.then((value) => value.path),
);
@@ -95,8 +95,11 @@ import 'common_operations.dart';
import 'expectation.dart';
import 'mock/mock_file_picker.dart';
const v020GridFileName = "v020.afdb";
const v069GridFileName = "v069.afdb";
extension AppFlowyDatabaseTest on WidgetTester {
Future<void> openV020database() async {
Future<void> openTestDatabase(String fileName) async {
final context = await initializeAppFlowy();
await tapAnonymousSignInButton();
@@ -106,29 +109,24 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapAddViewButton();
await tapImportButton();
final testFileNames = ['v020.afdb'];
final paths = <String>[];
for (final fileName in testFileNames) {
// Don't use the p.join to build the path that used in loadString. It
// is not working on windows.
final str = await rootBundle
.loadString("assets/test/workspaces/database/$fileName");
// Don't use the p.join to build the path that used in loadString. It
// is not working on windows.
final str = await rootBundle
.loadString("assets/test/workspaces/database/$fileName");
// Write the content to the file.
final path = p.join(
context.applicationDataDirectory,
fileName,
);
paths.add(path);
File(path).writeAsStringSync(str);
}
// Write the content to the file.
final path = p.join(
context.applicationDataDirectory,
fileName,
);
final pageName = p.basenameWithoutExtension(path);
File(path).writeAsStringSync(str);
// mock get files
mockPickFilePaths(
paths: paths,
paths: [path],
);
await tapDatabaseRawDataButton();
await pumpAndSettle();
await openPage('v020', layout: ViewLayoutPB.Grid);
await openPage(pageName, layout: ViewLayoutPB.Grid);
}
Future<void> hoverOnFirstRowOfGrid([Future<void> Function()? onHover]) async {
@@ -571,8 +569,19 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
/// Used to open the add cover popover, by pressing on "Add cover"-button.
///
/// Should call [hoverRowBanner] first.
///
Future<void> tapAddCoverButton() async {
await tapButtonWithName(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
await pumpAndSettle();
}
Future<void> openEmojiPicker() async =>
tapButton(find.byType(AddEmojiButton));
tapButton(find.text(LocaleKeys.document_plugins_cover_addIcon.tr()));
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(EditableDateCell);
@@ -787,7 +796,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(finder, findsWidgets);
}
Future<void> assertNumberOfRowsInGridPage(int num) async {
void assertNumberOfRowsInGridPage(int num) {
expect(
find.byType(GridRow, skipOffstage: false),
findsNWidgets(num),
@@ -872,21 +881,51 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(GridAddRowButton));
}
Future<void> tapCreateRowButtonInRowMenuOfGrid() async {
Future<void> tapCreateRowButtonAfterHoveringOnGridRow() async {
await tapButton(find.byType(InsertRowButton));
}
Future<void> tapRowMenuButtonInGrid() async {
expect(find.byType(RowMenuButton), findsOneWidget);
await tapButton(find.byType(RowMenuButton));
}
/// Should call [tapRowMenuButtonInGrid] first.
Future<void> tapCreateRowAboveButtonInRowMenu() async {
await tapButtonWithName(LocaleKeys.grid_row_insertRecordAbove.tr());
}
/// Should call [tapRowMenuButtonInGrid] first.
Future<void> tapDeleteOnRowMenu() async {
expect(find.text(LocaleKeys.grid_row_delete.tr()), findsOneWidget);
await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
}
Future<void> reorderRow(
String from,
String to,
) async {
final fromRow = find.byWidgetPredicate(
(widget) => widget is GridRow && widget.rowId == from,
);
final toRow = find.byWidgetPredicate(
(widget) => widget is GridRow && widget.rowId == to,
);
await hoverOnWidget(
fromRow,
onHover: () async {
final dragElement = find.descendant(
of: fromRow,
matching: find.byType(ReorderableDragStartListener),
);
await timedDrag(
dragElement,
getCenter(toRow) - getCenter(fromRow),
const Duration(milliseconds: 200),
);
await pumpAndSettle();
},
);
}
Future<void> createField(
FieldType fieldType, {
String? name,
@@ -1015,7 +1054,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// Must call [tapSortMenuInSettingBar] first.
Future<void> tapAllSortButton() async {
Future<void> tapDeleteAllSortsButton() async {
await tapButton(find.byType(DeleteAllSortsButton));
}
@@ -1252,7 +1291,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
matching: find.byType(EventCard),
);
await tapButton(cards.at(index));
await tapButton(cards.at(index), milliseconds: 1000);
}
void assertEventEditorOpen() =>
@@ -1268,6 +1307,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
);
await enterText(textField, title);
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle(const Duration(milliseconds: 300));
}
@@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
@@ -12,7 +13,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/uplo
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -33,6 +34,11 @@ class EditorOperations {
EditorState getCurrentEditorState() =>
tester.widget<AppFlowyEditor>(find.byType(AppFlowyEditor)).editorState;
Node getNodeAtPath(Path path) {
final editorState = getCurrentEditorState();
return editorState.getNodeAtPath(path)!;
}
/// Tap the line of editor at [index]
Future<void> tapLineOfEditorAt(int index) async {
final textBlocks = find.byType(AppFlowyRichText);
@@ -266,4 +272,55 @@ class EditorOperations {
},
);
}
/// Drag block
///
/// [offset] is the offset to move the block.
///
/// [path] is the path of the block to move.
Future<void> dragBlock(
Path path,
Offset offset,
) async {
final dragToMoveAction = find.byWidgetPredicate(
(widget) =>
widget is DraggableOptionButton &&
widget.blockComponentContext.node.path.equals(path),
);
await tester.hoverOnWidget(
dragToMoveAction,
onHover: () async {
final dragToMoveTooltip = find.findFlowyTooltip(
LocaleKeys.blockActions_dragTooltip.tr(),
);
await tester.pumpUntilFound(dragToMoveTooltip);
final location = tester.getCenter(dragToMoveAction);
final gesture = await tester.startGesture(
location,
pointer: 7,
);
await tester.pump();
// divide the steps to small move to avoid the drag area not found error
const steps = 5;
final stepOffset = Offset(offset.dx / steps, offset.dy / steps);
for (var i = 0; i < steps; i++) {
await gesture.moveBy(stepOffset);
await tester.pump(Durations.short1);
}
// check if the drag to move action is dragging
expect(
isDraggingAppFlowyEditorBlock.value,
isTrue,
);
await gesture.up();
await tester.pump();
},
);
await tester.pumpAndSettle(Durations.short1);
}
}
@@ -11,11 +11,11 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_platform/universal_platform.dart';
import 'util.dart';
@@ -184,7 +184,7 @@ extension Expectation on WidgetTester {
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
if (PlatformExtension.isDesktop) {
if (UniversalPlatform.isDesktop) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
@@ -1,9 +1,9 @@
export 'auth_operation.dart';
export 'base.dart';
export 'common_operations.dart';
export 'settings.dart';
export 'data.dart';
export 'document_test_operations.dart';
export 'expectation.dart';
export 'editor_test_operations.dart';
export 'mock/mock_url_launcher.dart';
export 'ime.dart';
export 'auth_operation.dart';
export 'mock/mock_url_launcher.dart';
export 'settings.dart';
+12 -6
View File
@@ -56,6 +56,8 @@ PODS:
- Flutter
- keyboard_height_plugin (0.0.1):
- Flutter
- open_file_ios (0.0.1):
- Flutter
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5):
@@ -102,6 +104,7 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@@ -148,6 +151,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/irondash_engine_context/ios"
keyboard_height_plugin:
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
@@ -170,7 +175,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
@@ -180,25 +185,26 @@ SPEC CHECKSUMS:
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_file_ios: 461db5853723763573e140de3193656f91990d9e
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -35,6 +35,7 @@ class KVKeys {
'kDocumentAppearanceCursorColor';
static const String kDocumentAppearanceSelectionColor =
'kDocumentAppearanceSelectionColor';
static const String kDocumentAppearanceWidth = 'kDocumentAppearanceWidth';
/// The key for saving the expanded views
///
@@ -391,17 +391,23 @@ class _AFDropdownMenuState<T> extends State<AFDropdownMenu<T>> {
);
}
// Remove the code here, it will throw a FlutterError
// Unless we upgrade to Flutter 3.24 https://github.com/flutter/flutter/issues/146764
void scrollToHighlight() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final BuildContext? highlightContext =
buttonItemKeys[currentHighlight!].currentContext;
if (highlightContext != null) {
Scrollable.ensureVisible(highlightContext);
}
},
debugLabel: 'DropdownMenu.scrollToHighlight',
);
// WidgetsBinding.instance.addPostFrameCallback(
// (_) {
// // try {
// final BuildContext? highlightContext =
// buttonItemKeys[currentHighlight!].currentContext;
// if (highlightContext != null) {
// Scrollable.ensureVisible(highlightContext);
// }
// } catch (_) {
// return;
// }
// },
// debugLabel: 'DropdownMenu.scrollToHighlight',
// );
}
double? getWidth(GlobalKey key) {
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/board.dart';
@@ -17,7 +19,6 @@ import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -235,29 +236,33 @@ class _BoardContentState extends State<_BoardContent> {
key: ValueKey(groupItemId),
margin: cardMargin,
decoration: _makeBoxDecoration(context),
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
child: BlocProvider.value(
value: boardBloc,
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
),
userProfile: boardBloc.userProfile,
),
),
);
@@ -21,6 +21,10 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dar
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -146,6 +150,58 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
text: LocaleKeys.button_duplicate.tr(),
),
const Divider(height: 8.5, thickness: 0.5),
MobileQuickActionButton(
onTap: () => showMobileBottomSheet(
context,
title: LocaleKeys.grid_media_addFileMobile.tr(),
showHeader: true,
showCloseButton: true,
showDragHandle: true,
builder: (dialogContext) => Container(
margin: const EdgeInsets.only(top: 12),
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: FileUploadMenu(
onInsertLocalFile: (files) async {
context
..pop()
..pop();
if (_bloc.state.currentRowId == null) {
return;
}
await insertLocalFiles(
context,
files,
userProfile: _bloc.userProfile,
documentId: _bloc.state.currentRowId!,
onUploadSuccess: (file, path, isLocalMode) {
_bloc.add(
MobileRowDetailEvent.addCover(
RowCoverPB(
data: path,
uploadType: isLocalMode
? FileUploadTypePB.LocalFile
: FileUploadTypePB.CloudFile,
coverType: CoverTypePB.FileCover,
),
),
);
},
);
},
onInsertNetworkFile: (url) async =>
_onInsertNetworkFile(url, context),
),
),
),
icon: FlowySvgs.add_cover_s,
text: 'Add cover',
),
const Divider(height: 8.5, thickness: 0.5),
MobileQuickActionButton(
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
text: LocaleKeys.button_delete.tr(),
@@ -178,6 +234,38 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
gravity: ToastGravity.BOTTOM,
);
}
Future<void> _onInsertNetworkFile(
String url,
BuildContext context,
) async {
context
..pop()
..pop();
if (url.isEmpty) return;
final uri = Uri.tryParse(url);
if (uri == null) {
return;
}
String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : "";
if (name.isEmpty && uri.pathSegments.length > 1) {
name = uri.pathSegments[uri.pathSegments.length - 2];
} else if (name.isEmpty) {
name = uri.host;
}
_bloc.add(
MobileRowDetailEvent.addCover(
RowCoverPB(
data: url,
uploadType: FileUploadTypePB.NetworkFile,
coverType: CoverTypePB.FileCover,
),
),
);
}
}
class RowDetailFab extends StatelessWidget {
@@ -322,91 +410,129 @@ class MobileRowDetailPageContentState
rowController: rowController,
),
child: BlocBuilder<RowDetailBloc, RowDetailState>(
builder: (context, rowDetailState) {
return Column(
children: [
BlocProvider<RowBannerBloc>(
create: (context) => RowBannerBloc(
viewId: viewId,
fieldController: fieldController,
rowMeta: rowController.rowMeta,
)..add(const RowBannerEvent.initial()),
child: BlocConsumer<RowBannerBloc, RowBannerState>(
listener: (context, state) {
if (state.primaryField == null) {
return;
}
primaryFieldId.value = state.primaryField!.id;
},
builder: (context, state) {
if (state.primaryField == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: cellBuilder.buildCustom(
CellContext(
rowId: rowController.rowId,
fieldId: state.primaryField!.id,
),
skinMap: EditableCellSkinMap(
textSkin: _TitleSkin(),
),
builder: (context, rowDetailState) => Column(
children: [
if (rowDetailState.rowMeta.cover.data.isNotEmpty) ...[
GestureDetector(
onTap: () => showMobileBottomSheet(
context,
backgroundColor: AFThemeExtension.of(context).background,
showDragHandle: true,
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
MobileQuickActionButton(
onTap: () {
context
..pop()
..read<RowDetailBloc>()
.add(const RowDetailEvent.removeCover());
},
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
icon: FlowySvgs.trash_s,
iconColor: Theme.of(context).colorScheme.error,
),
);
},
],
),
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 9, bottom: 100),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: MobileRowPropertyList(
databaseController: widget.databaseController,
cellBuilder: cellBuilder,
),
child: SizedBox(
height: 200,
width: double.infinity,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
),
Padding(
padding: const EdgeInsets.fromLTRB(6, 6, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (rowDetailState.numHiddenFields != 0) ...[
const ToggleHiddenFieldsVisibilityButton(),
],
const VSpace(8.0),
ValueListenableBuilder(
valueListenable: primaryFieldId,
builder: (context, primaryFieldId, child) {
if (primaryFieldId.isEmpty) {
return const SizedBox.shrink();
}
return OpenRowPageButton(
databaseController: widget.databaseController,
cellContext: CellContext(
rowId: rowController.rowId,
fieldId: primaryFieldId,
),
documentId: rowController.rowMeta.documentId,
);
},
),
MobileRowDetailCreateFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
child: AFImage(
url: rowDetailState.rowMeta.cover.data,
uploadType: widget.rowMeta.cover.uploadType,
userProfile:
context.read<MobileRowDetailBloc>().userProfile,
),
],
),
),
),
],
);
},
BlocProvider<RowBannerBloc>(
create: (context) => RowBannerBloc(
viewId: viewId,
fieldController: fieldController,
rowMeta: rowController.rowMeta,
)..add(const RowBannerEvent.initial()),
child: BlocConsumer<RowBannerBloc, RowBannerState>(
listener: (context, state) {
if (state.primaryField == null) {
return;
}
primaryFieldId.value = state.primaryField!.id;
},
builder: (context, state) {
if (state.primaryField == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: cellBuilder.buildCustom(
CellContext(
rowId: rowController.rowId,
fieldId: state.primaryField!.id,
),
skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),
),
);
},
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 9, bottom: 100),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: MobileRowPropertyList(
databaseController: widget.databaseController,
cellBuilder: cellBuilder,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(6, 6, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (rowDetailState.numHiddenFields != 0) ...[
const ToggleHiddenFieldsVisibilityButton(),
],
const VSpace(8.0),
ValueListenableBuilder(
valueListenable: primaryFieldId,
builder: (context, primaryFieldId, child) {
if (primaryFieldId.isEmpty) {
return const SizedBox.shrink();
}
return OpenRowPageButton(
databaseController: widget.databaseController,
cellContext: CellContext(
rowId: rowController.rowId,
fieldId: primaryFieldId,
),
documentId: rowController.rowMeta.documentId,
);
},
),
MobileRowDetailCreateFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
),
],
),
),
],
),
),
);
}
@@ -429,7 +555,9 @@ class _TitleSkin extends IEditableTextCellSkin {
fontSize: 23,
fontWeight: FontWeight.w500,
),
onChanged: (text) => bloc.add(TextCellEvent.updateText(text)),
onEditingComplete: () {
bloc.add(TextCellEvent.updateText(textEditingController.text));
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 9),
border: InputBorder.none,
@@ -90,7 +90,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
),
onPressed: () {
final name = state.content;
_openRowPage(context, name);
_openRowPage(context, name ?? "");
},
),
);
@@ -1,16 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
class MobileCardContent extends StatelessWidget {
const MobileCardContent({
@@ -19,57 +14,38 @@ class MobileCardContent extends StatelessWidget {
required this.cellBuilder,
required this.cells,
required this.styleConfiguration,
required this.userProfile,
});
final RowMetaPB rowMeta;
final CardCellBuilder cellBuilder;
final List<CellMeta> cells;
final RowCardStyleConfiguration styleConfiguration;
final UserProfilePB? userProfile;
@override
Widget build(BuildContext context) {
final attachmentCount = rowMeta.attachmentCount.toInt();
return Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...cells.map(
(cellMeta) {
return cellBuilder.build(
cellContext: cellMeta.cellContext(),
styleMap: mobileBoardCardCellStyleMap(context),
hasNotes: !rowMeta.isDocumentEmpty,
);
},
),
if (attachmentCount > 0) ...[
const VSpace(4),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Row(
children: [
const FlowySvg(
FlowySvgs.media_s,
size: Size.square(12),
),
const HSpace(6),
Flexible(
child: FlowyText.regular(
LocaleKeys.grid_media_attachmentsHint
.tr(args: ['$attachmentCount']),
fontSize: 12,
color: AFThemeExtension.of(context).secondaryTextColor,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (rowMeta.cover.data.isNotEmpty) ...[
CardCover(cover: rowMeta.cover, userProfile: userProfile),
],
),
Padding(
padding: styleConfiguration.cardPadding,
child: Column(
children: [
...cells.map(
(cellMeta) => cellBuilder.build(
cellContext: cellMeta.cellContext(),
styleMap: mobileBoardCardCellStyleMap(context),
hasNotes: !rowMeta.isDocumentEmpty,
),
),
],
),
),
],
);
}
}
@@ -20,7 +20,7 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -198,7 +198,6 @@ class _HomePageState extends State<_HomePage> {
padding: const EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: 8.0,
),
child: MobileHomePageHeader(
userProfile: widget.userProfile,
@@ -226,11 +225,11 @@ class _HomePageState extends State<_HomePage> {
FavoriteBloc()..add(const FavoriteEvent.initial()),
),
BlocProvider(
create: (_) => SpaceBloc()
..add(
SpaceEvent.initial(
widget.userProfile,
workspaceId,
create: (_) => SpaceBloc(
userProfile: widget.userProfile,
workspaceId: workspaceId,
)..add(
const SpaceEvent.initial(
openFirstPage: false,
),
),
@@ -5,13 +5,13 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' show PlatformExtension;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:universal_platform/universal_platform.dart';
class MobileMemberList extends StatelessWidget {
const MobileMemberList({
@@ -75,7 +75,7 @@ class _MemberItem extends StatelessWidget {
Widget child;
if (PlatformExtension.isDesktop) {
if (UniversalPlatform.isDesktop) {
child = Row(
children: [
Expanded(
@@ -14,7 +14,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/platform_extension.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -23,6 +22,7 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'package:styled_widget/styled_widget.dart';
import 'package:universal_platform/universal_platform.dart';
import 'application/chat_member_bloc.dart';
import 'application/chat_side_pannel_bloc.dart';
@@ -152,7 +152,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
@override
Widget build(BuildContext context) {
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
if (PlatformExtension.isDesktop) {
if (UniversalPlatform.isDesktop) {
return BlocSelector<ChatSidePannelBloc, ChatSidePannelState, bool>(
selector: (state) => state.isShowPannel,
builder: (context, isShowPannel) {
@@ -228,7 +228,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
}
Widget buildChatSidePannel() {
if (PlatformExtension.isDesktop) {
if (UniversalPlatform.isDesktop) {
return BlocBuilder<ChatSidePannelBloc, ChatSidePannelState>(
builder: (context, state) {
if (state.metadata != null) {
@@ -425,7 +425,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
},
),
const VSpace(6),
if (PlatformExtension.isDesktop)
if (UniversalPlatform.isDesktop)
Opacity(
opacity: 0.6,
child: FlowyText(
@@ -10,7 +10,6 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra/platform_extension.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
@@ -19,6 +18,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:universal_platform/universal_platform.dart';
import 'chat_at_button.dart';
import 'chat_input_attachment.dart';
@@ -68,7 +68,7 @@ class _ChatInputState extends State<ChatInput> {
_textController = InputTextFieldController();
_inputFocusNode = FocusNode(
onKeyEvent: (node, event) {
if (PlatformExtension.isDesktop) {
if (UniversalPlatform.isDesktop) {
if (_inputActionControl.canHandleKeyEvent(event)) {
_inputActionControl.handleKeyEvent(event);
return KeyEventResult.handled;
@@ -154,7 +154,7 @@ class _ChatInputState extends State<ChatInput> {
Row(
children: [
// TODO(lucas): support mobile
if (PlatformExtension.isDesktop &&
if (UniversalPlatform.isDesktop &&
widget.aiType.isLocalAI())
_attachmentButton(buttonPadding),
@@ -164,7 +164,7 @@ class _ChatInputState extends State<ChatInput> {
// mention button
_mentionButton(buttonPadding),
if (PlatformExtension.isMobile) const HSpace(6.0),
if (UniversalPlatform.isMobile) const HSpace(6.0),
// send button
_sendButton(buttonPadding),
@@ -274,7 +274,7 @@ class _ChatInputState extends State<ChatInput> {
return;
}
if (PlatformExtension.isDesktop) {
if (UniversalPlatform.isDesktop) {
ChatActionsMenu(
anchor: ChatInputAnchor(
anchorKey: _textFieldKey,
@@ -1,8 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:universal_platform/universal_platform.dart';
class FlowyEmojiHeader extends StatelessWidget {
const FlowyEmojiHeader({
@@ -14,7 +14,7 @@ class FlowyEmojiHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (PlatformExtension.isDesktopOrWeb) {
if (UniversalPlatform.isDesktop) {
return Container(
height: 22,
color: Theme.of(context).cardColor,
@@ -2,10 +2,12 @@ import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flowy_infra/uuid.dart';
@@ -58,7 +60,15 @@ class MediaCellBloc extends Bloc<MediaCellEvent, MediaCellState> {
emit(state.copyWith(files: files));
},
didUpdateField: (fieldName) {
emit(state.copyWith(fieldName: fieldName));
final typeOption =
cellController.getTypeOption(MediaTypeOptionDataParser());
emit(
state.copyWith(
fieldName: fieldName,
hideFileNames: typeOption.hideFileNames,
),
);
},
addFile: (url, name, uploadType, fileType) async {
final newFile = MediaFilePB(
@@ -185,7 +195,7 @@ class MediaCellEvent with _$MediaCellEvent {
const factory MediaCellEvent.addFile({
required String url,
required String name,
required MediaUploadTypePB uploadType,
required FileUploadTypePB uploadType,
required MediaFileTypePB fileType,
}) = _AddFile;
@@ -213,14 +223,18 @@ class MediaCellState with _$MediaCellState {
required String fieldName,
@Default([]) List<MediaFilePB> files,
@Default(false) showAllFiles,
@Default(false) hideFileNames,
}) = _MediaCellState;
factory MediaCellState.initial(MediaCellController cellController) {
final cellData = cellController.getCellData();
final typeOption =
cellController.getTypeOption(MediaTypeOptionDataParser());
return MediaCellState(
fieldName: cellController.fieldInfo.field.name,
files: cellData?.files ?? const [],
hideFileNames: typeOption.hideFileNames,
);
}
}
@@ -34,7 +34,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
on<TextCellEvent>(
(event, emit) {
event.when(
didReceiveCellUpdate: (String content) {
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
didUpdateField: (fieldInfo) {
@@ -44,7 +44,9 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
}
},
updateText: (String text) {
if (state.content != text) {
// If the content is null, it indicates that either the cell is empty (no data)
// or the cell data is still being fetched from the backend and is not yet available.
if (state.content != null && state.content != text) {
cellController.saveCellData(text, debounce: true);
}
},
@@ -60,7 +62,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
_onCellChangedFn = cellController.addListener(
onCellChanged: (cellContent) {
if (!isClosed) {
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
add(TextCellEvent.didReceiveCellUpdate(cellContent));
}
},
onFieldChanged: _onFieldChangedListener,
@@ -76,7 +78,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
@freezed
class TextCellEvent with _$TextCellEvent {
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
const factory TextCellEvent.didReceiveCellUpdate(String? cellContent) =
_DidReceiveCellUpdate;
const factory TextCellEvent.didUpdateField(FieldInfo fieldInfo) =
_DidUpdateField;
@@ -87,7 +89,7 @@ class TextCellEvent with _$TextCellEvent {
@freezed
class TextCellState with _$TextCellState {
const factory TextCellState({
required String content,
required String? content,
required ValueNotifier<String>? emoji,
required ValueNotifier<bool>? hasDocument,
required bool enableEdit,
@@ -95,7 +97,7 @@ class TextCellState with _$TextCellState {
}) = _TextCellState;
factory TextCellState.initial(TextCellController cellController) {
final cellData = cellController.getCellData() ?? "";
final cellData = cellController.getCellData();
final wrap = cellController.fieldInfo.wrapCellContent ?? true;
ValueNotifier<String>? emoji;
ValueNotifier<bool>? hasDocument;
@@ -86,6 +86,7 @@ class FieldInfo with _$FieldInfo {
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
case FieldType.Checklist:
case FieldType.URL:
case FieldType.Time:
return true;
default:
@@ -57,3 +57,10 @@ class TranslateTypeOptionDataParser
return TranslateTypeOptionPB.fromBuffer(buffer);
}
}
class MediaTypeOptionDataParser extends TypeOptionParser<MediaTypeOptionPB> {
@override
MediaTypeOptionPB fromBuffer(List<int> buffer) {
return MediaTypeOptionPB.fromBuffer(buffer);
}
}
@@ -1,11 +1,14 @@
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../database_controller.dart';
import 'row_controller.dart';
part 'related_row_detail_bloc.freezed.dart';
@@ -20,6 +23,9 @@ class RelatedRowDetailPageBloc
_init(databaseId, initialRowId);
}
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
@override
Future<void> close() {
state.whenOrNull(
@@ -33,11 +39,17 @@ class RelatedRowDetailPageBloc
void _dispatch() {
on<RelatedRowDetailPageEvent>((event, emit) async {
event.when(
didInitialize: (databaseController, rowController) {
rowController.initialize();
await event.when(
didInitialize: (databaseController, rowController) async {
final response = await UserEventGetUserProfile().send();
response.fold(
(userProfile) => _userProfile = userProfile,
(err) => Log.error(err),
);
state.maybeWhen(
await rowController.initialize();
await state.maybeWhen(
ready: (_, oldRowController) async {
await oldRowController.dispose();
emit(
@@ -1,8 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -26,6 +31,11 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
final RowBackendService _rowBackendSvc;
final RowMetaListener _metaListener;
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
bool get hasCover => state.rowMeta.cover.data.isNotEmpty;
@override
Future<void> close() async {
await _metaListener.stop();
@@ -36,15 +46,21 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
on<RowBannerEvent>(
(event, emit) {
event.when(
initial: () {
_loadPrimaryField();
initial: () async {
await _loadPrimaryField();
_listenRowMetaChanged();
final result = await UserEventGetUserProfile().send();
result.fold(
(userProfile) => _userProfile = userProfile,
(error) => Log.error(error),
);
},
didReceiveRowMeta: (RowMetaPB rowMeta) {
emit(state.copyWith(rowMeta: rowMeta));
},
setCover: (String coverURL) => _updateMeta(coverURL: coverURL),
setCover: (RowCoverPB cover) => _updateMeta(cover: cover),
setIcon: (String iconURL) => _updateMeta(iconURL: iconURL),
removeCover: () => _removeCover(),
didReceiveFieldUpdate: (updatedField) {
emit(
state.copyWith(
@@ -91,14 +107,19 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
}
/// Update the meta of the row and the view
Future<void> _updateMeta({String? iconURL, String? coverURL}) async {
Future<void> _updateMeta({String? iconURL, RowCoverPB? cover}) async {
final result = await _rowBackendSvc.updateMeta(
iconURL: iconURL,
coverURL: coverURL,
cover: cover,
rowId: state.rowMeta.id,
);
result.fold((l) => null, (err) => Log.error(err));
}
Future<void> _removeCover() async {
final result = await _rowBackendSvc.removeCover(state.rowMeta.id);
result.fold((l) => null, (err) => Log.error(err));
}
}
@freezed
@@ -109,11 +130,14 @@ class RowBannerEvent with _$RowBannerEvent {
const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
const factory RowBannerEvent.setCover(RowCoverPB cover) = _SetCover;
const factory RowBannerEvent.removeCover() = _RemoveCover;
}
@freezed
class RowBannerState with _$RowBannerState {
class RowBannerState extends Equatable with _$RowBannerState {
const RowBannerState._();
const factory RowBannerState({
required FieldPB? primaryField,
required RowMetaPB rowMeta,
@@ -125,6 +149,14 @@ class RowBannerState with _$RowBannerState {
rowMeta: rowMetaPB,
loadingState: const LoadingState.loading(),
);
@override
List<Object?> get props => [
rowMeta.cover.data,
rowMeta.icon,
primaryField,
loadingState,
];
}
@freezed
@@ -1,5 +1,6 @@
import 'dart:collection';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
@@ -315,7 +316,7 @@ class RowChangesetNotifier extends ChangeNotifier {
}
}
class RowInfo {
class RowInfo extends Equatable {
RowInfo({
required this.fields,
required RowMetaPB rowMeta,
@@ -347,6 +348,9 @@ class RowInfo {
rowIconNotifier.dispose();
rowDocumentNotifier.dispose();
}
@override
List<Object> get props => [rowMeta];
}
typedef InsertedIndexs = List<InsertedIndex>;
@@ -32,7 +32,7 @@ class RowController {
final RowBackendService _rowBackendSvc;
bool _isDisposed = false;
String get rowId => rowMeta.id;
String get rowId => _rowMeta.id;
RowMetaPB get rowMeta => _rowMeta;
CellMemCache get cellCache => _rowCache.cellCache;
@@ -83,7 +83,7 @@ class RowBackendService {
Future<FlowyResult<void, FlowyError>> updateMeta({
required String rowId,
String? iconURL,
String? coverURL,
RowCoverPB? cover,
bool? isDocumentEmpty,
}) {
final payload = UpdateRowMetaChangesetPB.create()
@@ -93,8 +93,8 @@ class RowBackendService {
if (iconURL != null) {
payload.iconUrl = iconURL;
}
if (coverURL != null) {
payload.coverUrl = coverURL;
if (cover != null) {
payload.cover = cover;
}
if (isDocumentEmpty != null) {
@@ -104,6 +104,14 @@ class RowBackendService {
return DatabaseEventUpdateRowMeta(payload).send();
}
Future<FlowyResult<void, FlowyError>> removeCover(String rowId) async {
final payload = RemoveCoverPayloadPB.create()
..viewId = viewId
..rowId = rowId;
return DatabaseEventRemoveCover(payload).send();
}
static Future<FlowyResult<void, FlowyError>> deleteRows(
String viewId,
List<RowId> rowIds,
@@ -6,10 +6,10 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:universal_platform/universal_platform.dart';
import 'database_controller.dart';
@@ -249,7 +249,7 @@ class DatabaseTabBarState with _$DatabaseTabBarState {
class DatabaseTabBar extends Equatable {
DatabaseTabBar({
required this.view,
}) : _builder = PlatformExtension.isMobile
}) : _builder = UniversalPlatform.isMobile
? view.mobileTabBarItem()
: view.tabBarItem();
@@ -1,29 +1,29 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/database/application/defines.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/board/group_ext.dart';
import 'package:appflowy/plugins/database/domain/group_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import 'package:universal_platform/universal_platform.dart';
import '../../application/database_controller.dart';
import '../../application/field/field_controller.dart';
import '../../application/row/row_cache.dart';
import 'group_controller.dart';
part 'board_bloc.freezed.dart';
@@ -49,6 +49,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
late final GroupBackendService groupBackendSvc;
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId;
@@ -96,6 +99,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
emit(BoardState.initial(viewId));
_startListening();
await _openDatabase(emit);
final result = await UserEventGetUserProfile().send();
result.fold(
(profile) => _userProfile = profile,
(err) => Log.error('Failed to fetch user profile: ${err.msg}'),
);
},
createRow: (groupId, position, title, targetRowId) async {
final primaryField = databaseController.fieldController.fieldInfos
@@ -113,7 +122,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
final startEditing = position != OrderObjectPositionTypePB.End;
final action = PlatformExtension.isMobile
final action = UniversalPlatform.isMobile
? DidCreateRowAction.openAsPage
: startEditing
? DidCreateRowAction.startEditing
@@ -143,6 +152,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
final result = await groupBackendSvc.deleteGroup(groupId: groupId);
result.fold((_) {}, (err) => Log.error(err));
},
renameGroup: (groupId, name) async {
final result = await groupBackendSvc.updateGroup(
groupId: groupId,
name: name,
);
result.fold((_) {}, (err) => Log.error(err));
},
didReceiveError: (error) {
emit(BoardState.error(error: error));
},
@@ -207,7 +223,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
final currentName = group.generateGroupName(databaseController);
if (currentName != groupName) {
await groupBackendSvc.updateGroup(
fieldId: groupControllers.values.first.group.fieldId,
groupId: groupId,
name: groupName,
);
@@ -282,7 +297,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
} else {
await groupBackendSvc.updateGroup(
fieldId: groupControllers.values.first.group.fieldId,
groupId: group.groupId,
visible: isVisible,
);
@@ -550,6 +564,8 @@ class BoardEvent with _$BoardEvent {
) = _SetGroupVisibility;
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
_ToggleHiddenSectionVisibility;
const factory BoardEvent.renameGroup(String groupId, String name) =
_RenameGroup;
const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup;
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
_ReorderGroup;
@@ -718,7 +734,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
}
class GroupData {
GroupData({
const GroupData({
required this.group,
required this.fieldInfo,
});
@@ -1,113 +0,0 @@
import 'package:appflowy/plugins/database/board/group_ext.dart';
import 'package:appflowy/plugins/database/domain/group_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../application/database_controller.dart';
import '../../application/field/field_controller.dart';
part 'column_header_bloc.freezed.dart';
class ColumnHeaderBloc extends Bloc<ColumnHeaderEvent, ColumnHeaderState> {
ColumnHeaderBloc({
required this.databaseController,
required this.fieldId,
required this.group,
}) : super(const ColumnHeaderState.loading()) {
groupBackendSvc = GroupBackendService(viewId);
_dispatch();
}
final DatabaseController databaseController;
final String fieldId;
final GroupPB group;
late final GroupBackendService groupBackendSvc;
FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId;
void _dispatch() {
on<ColumnHeaderEvent>(
(event, emit) async {
await event.when(
initial: () {
final name = group.generateGroupName(databaseController);
emit(ColumnHeaderState.initial(name));
},
startEditing: () async {
state.maybeMap(
ready: (state) => emit(state.copyWith(isEditing: true)),
orElse: () {},
);
},
endEditing: (String? groupName) async {
if (groupName != null) {
final stateGroupName = state.maybeMap(
ready: (state) => state.groupName,
orElse: () => null,
);
if (stateGroupName == null || stateGroupName == groupName) {
state.maybeMap(
ready: (state) => emit(
state.copyWith(
groupName: stateGroupName!,
isEditing: false,
),
),
orElse: () {},
);
}
await groupBackendSvc.renameGroup(
groupId: group.groupId,
fieldId: fieldId,
name: groupName,
);
state.maybeMap(
ready: (state) {
emit(state.copyWith(groupName: groupName, isEditing: false));
},
orElse: () {},
);
}
},
);
},
);
}
}
@freezed
class ColumnHeaderEvent with _$ColumnHeaderEvent {
const factory ColumnHeaderEvent.initial() = _Initial;
const factory ColumnHeaderEvent.startEditing() = _StartEditing;
const factory ColumnHeaderEvent.endEditing(String? groupName) = _EndEditing;
}
@freezed
class ColumnHeaderState with _$ColumnHeaderState {
const ColumnHeaderState._();
const factory ColumnHeaderState.loading() = _ColumnHeaderLoadingState;
const factory ColumnHeaderState.error({
required FlowyError error,
}) = _ColumnHeaderErrorState;
const factory ColumnHeaderState.ready({
required String groupName,
@Default(false) bool isEditing,
@Default(false) bool canEdit,
}) = _ColumnHeaderReadyState;
factory ColumnHeaderState.initial(String name) =>
ColumnHeaderState.ready(groupName: name);
bool get isLoading => maybeMap(loading: (_) => true, orElse: () => false);
bool get isError => maybeMap(error: (_) => true, orElse: () => false);
bool get isReady => maybeMap(ready: (_) => true, orElse: () => false);
}
@@ -1,12 +1,14 @@
import 'dart:io';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_page.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';
import 'package:appflowy/plugins/database/board/application/column_header_bloc.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
@@ -20,18 +22,17 @@ import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../widgets/card/card.dart';
import '../../widgets/cell/card_cell_builder.dart';
import '../application/board_bloc.dart';
import 'toolbar/board_setting_bar.dart';
import 'widgets/board_focus_scope.dart';
import 'widgets/board_hidden_groups.dart';
@@ -48,7 +49,7 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
bool shrinkWrap,
String? initialRowId,
) =>
PlatformExtension.isDesktop
UniversalPlatform.isDesktop
? DesktopBoardPage(
key: _makeValueKey(controller),
view: view,
@@ -341,23 +342,10 @@ class _BoardContentState extends State<_BoardContent> {
false
? BoardTrailing(scrollController: scrollController)
: const HSpace(40),
headerBuilder: (_, groupData) => MultiBlocProvider(
providers: [
BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
),
BlocProvider<ColumnHeaderBloc>(
create: (context) => ColumnHeaderBloc(
databaseController: databaseController,
fieldId: (groupData.customData as GroupData).fieldInfo.id,
group: context
.read<BoardBloc>()
.groupControllers[groupData.headerData.groupId]!
.group,
)..add(const ColumnHeaderEvent.initial()),
),
],
headerBuilder: (_, groupData) => BlocProvider.value(
value: context.read<BoardBloc>(),
child: BoardColumnHeader(
databaseController: databaseController,
groupData: groupData,
margin: config.groupHeaderPadding,
),
@@ -701,6 +689,7 @@ class _BoardCardState extends State<_BoardCard> {
rowId: rowMeta.id,
),
),
userProfile: context.read<BoardBloc>().userProfile,
),
),
),
@@ -866,6 +855,7 @@ void _openCard({
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
),
);
}
@@ -0,0 +1,58 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/board/group_ext.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'board_column_header.dart';
class CheckboxColumnHeader extends StatelessWidget {
const CheckboxColumnHeader({
super.key,
required this.databaseController,
required this.groupData,
});
final DatabaseController databaseController;
final AppFlowyGroupData groupData;
@override
Widget build(BuildContext context) {
final customData = groupData.customData as GroupData;
final groupName = customData.group.generateGroupName(databaseController);
return Row(
children: [
FlowySvg(
customData.asCheckboxGroup()!.isCheck
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(18),
),
const HSpace(6),
Expanded(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowyTooltip(
message: groupName,
child: FlowyText.medium(
groupName,
overflow: TextOverflow.ellipsis,
),
),
),
),
const HSpace(6),
GroupOptionsButton(
groupData: groupData,
),
const HSpace(4),
CreateCardFromTopButton(
groupId: groupData.id,
),
],
);
}
}
@@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/board/application/column_header_bloc.dart';
import 'package:appflowy/plugins/database/board/group_ext.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
@@ -13,15 +11,21 @@ import 'package:appflowy_board/appflowy_board.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 'board_checkbox_column_header.dart';
import 'board_editable_column_header.dart';
class BoardColumnHeader extends StatefulWidget {
const BoardColumnHeader({
super.key,
required this.databaseController,
required this.groupData,
required this.margin,
});
final DatabaseController databaseController;
final AppFlowyGroupData groupData;
final EdgeInsets margin;
@@ -30,190 +34,61 @@ class BoardColumnHeader extends StatefulWidget {
}
class _BoardColumnHeaderState extends State<BoardColumnHeader> {
final FocusNode _focusNode = FocusNode();
final FocusNode _keyboardListenerFocusNode = FocusNode();
final ValueNotifier<bool> isEditing = ValueNotifier(false);
late final TextEditingController _controller =
TextEditingController.fromValue(
TextEditingValue(
selection: TextSelection.collapsed(
offset: widget.groupData.headerData.groupName.length,
),
text: widget.groupData.headerData.groupName,
),
);
@override
void initState() {
super.initState();
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
_saveEdit();
}
});
}
GroupData get customData => widget.groupData.customData;
@override
void dispose() {
_focusNode.dispose();
_keyboardListenerFocusNode.dispose();
_controller.dispose();
isEditing.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final boardCustomData = widget.groupData.customData as GroupData;
return BlocBuilder<ColumnHeaderBloc, ColumnHeaderState>(
builder: (context, state) {
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
if (state.isEditing) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
Widget title = Expanded(
child: FlowyText.medium(
state.groupName,
overflow: TextOverflow.ellipsis,
),
);
if (!boardCustomData.group.isDefault &&
boardCustomData.fieldType.canEditHeader) {
title = Flexible(
fit: FlexFit.tight,
child: FlowyTooltip(
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => context
.read<ColumnHeaderBloc>()
.add(const ColumnHeaderEvent.startEditing()),
child: FlowyText.medium(
state.groupName,
overflow: TextOverflow.ellipsis,
),
),
),
),
);
}
if (state.isEditing) {
title = _buildTextField(context);
}
return Padding(
padding: widget.margin,
child: SizedBox(
height: 50,
child: Row(
children: [
_buildHeaderIcon(boardCustomData),
title,
const HSpace(6),
_groupOptionsButton(context),
const HSpace(4),
FlowyTooltip(
message:
LocaleKeys.board_column_addToColumnTopTooltip.tr(),
preferBelow: false,
child: FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.add_s),
iconColorOnHover:
Theme.of(context).colorScheme.onSurface,
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
widget.groupData.id,
OrderObjectPositionTypePB.Start,
null,
null,
),
),
),
),
],
),
),
);
final Widget child = switch (customData.fieldType) {
FieldType.MultiSelect ||
FieldType.SingleSelect when !customData.group.isDefault =>
EditableColumnHeader(
databaseController: widget.databaseController,
groupData: widget.groupData,
isEditing: isEditing,
onSubmitted: (columnName) {
context
.read<BoardBloc>()
.add(BoardEvent.renameGroup(widget.groupData.id, columnName));
},
);
},
);
}
Widget _buildTextField(BuildContext context) {
return Expanded(
child: KeyboardListener(
focusNode: _keyboardListenerFocusNode,
onKeyEvent: (event) {
if ([LogicalKeyboardKey.enter, LogicalKeyboardKey.escape]
.contains(event.logicalKey)) {
_saveEdit();
}
},
child: TextField(
controller: _controller,
focusNode: _focusNode,
onEditingComplete: _saveEdit,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 14),
decoration: InputDecoration(
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
hoverColor: Colors.transparent,
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
isDense: true,
),
),
),
FieldType.Checkbox => CheckboxColumnHeader(
databaseController: widget.databaseController,
groupData: widget.groupData,
),
_ => _DefaultColumnHeaderContent(
databaseController: widget.databaseController,
groupData: widget.groupData,
),
};
return Container(
padding: widget.margin,
height: 50,
child: child,
);
}
}
void _saveEdit() {
context
.read<ColumnHeaderBloc>()
.add(ColumnHeaderEvent.endEditing(_controller.text));
}
class GroupOptionsButton extends StatelessWidget {
const GroupOptionsButton({
super.key,
required this.groupData,
this.isEditing,
});
Widget _buildHeaderIcon(GroupData customData) =>
switch (customData.fieldType) {
FieldType.Checkbox => Padding(
padding: const EdgeInsets.only(right: 6),
child: FlowySvg(
customData.asCheckboxGroup()!.isCheck
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(18),
),
),
_ => const SizedBox.shrink(),
};
final AppFlowyGroupData groupData;
final ValueNotifier<bool>? isEditing;
Widget _groupOptionsButton(BuildContext context) {
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
clickHandler: PopoverClickHandler.gestureDetector,
margin: const EdgeInsets.all(8),
@@ -225,14 +100,14 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
),
popupBuilder: (popoverContext) {
final customGroupData = widget.groupData.customData as GroupData;
final customGroupData = groupData.customData as GroupData;
final isDefault = customGroupData.group.isDefault;
final menuItems = GroupOptions.values.toList();
final menuItems = GroupOption.values.toList();
if (!customGroupData.fieldType.canEditHeader || isDefault) {
menuItems.remove(GroupOptions.rename);
menuItems.remove(GroupOption.rename);
}
if (!customGroupData.fieldType.canDeleteGroup || isDefault) {
menuItems.remove(GroupOptions.delete);
menuItems.remove(GroupOption.delete);
}
return SeparatedColumn(
mainAxisSize: MainAxisSize.min,
@@ -249,7 +124,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
overflow: TextOverflow.ellipsis,
),
onTap: () {
action.call(context, customGroupData.group);
run(context, action, customGroupData.group);
PopoverContainer.of(popoverContext).close();
},
),
@@ -260,37 +135,107 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
},
);
}
}
enum GroupOptions {
rename,
hide,
delete;
void call(BuildContext context, GroupPB group) {
switch (this) {
case rename:
context
.read<ColumnHeaderBloc>()
.add(const ColumnHeaderEvent.startEditing());
void run(BuildContext context, GroupOption option, GroupPB group) {
switch (option) {
case GroupOption.rename:
isEditing?.value = true;
break;
case hide:
case GroupOption.hide:
context
.read<BoardBloc>()
.add(BoardEvent.setGroupVisibility(group, false));
break;
case delete:
NavigatorAlertDialog(
title: LocaleKeys.board_column_deleteColumnConfirmation.tr(),
confirm: () {
case GroupOption.delete:
showConfirmDeletionDialog(
context: context,
name: LocaleKeys.board_column_label.tr(),
description: LocaleKeys.board_column_deleteColumnConfirmation.tr(),
onConfirm: () {
context
.read<BoardBloc>()
.add(BoardEvent.deleteGroup(group.groupId));
},
).show(context);
);
break;
}
}
}
class CreateCardFromTopButton extends StatelessWidget {
const CreateCardFromTopButton({
super.key,
required this.groupId,
});
final String groupId;
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: LocaleKeys.board_column_addToColumnTopTooltip.tr(),
preferBelow: false,
child: FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.add_s),
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
groupId,
OrderObjectPositionTypePB.Start,
null,
null,
),
),
),
);
}
}
class _DefaultColumnHeaderContent extends StatelessWidget {
const _DefaultColumnHeaderContent({
required this.databaseController,
required this.groupData,
});
final DatabaseController databaseController;
final AppFlowyGroupData groupData;
@override
Widget build(BuildContext context) {
final customData = groupData.customData as GroupData;
final groupName = customData.group.generateGroupName(databaseController);
return Row(
children: [
Expanded(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowyTooltip(
message: groupName,
child: FlowyText.medium(
groupName,
overflow: TextOverflow.ellipsis,
),
),
),
),
const HSpace(6),
GroupOptionsButton(
groupData: groupData,
),
const HSpace(4),
CreateCardFromTopButton(
groupId: groupData.id,
),
],
);
}
}
enum GroupOption {
rename,
hide,
delete;
FlowySvgData get icon => switch (this) {
rename => FlowySvgs.edit_s,
@@ -0,0 +1,258 @@
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/board/group_ext.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'board_column_header.dart';
/// This column header is used for the MultiSelect and SingleSelect field types.
class EditableColumnHeader extends StatefulWidget {
const EditableColumnHeader({
super.key,
required this.databaseController,
required this.groupData,
required this.isEditing,
required this.onSubmitted,
});
final DatabaseController databaseController;
final AppFlowyGroupData groupData;
final ValueNotifier<bool> isEditing;
final void Function(String columnName) onSubmitted;
@override
State<EditableColumnHeader> createState() => _EditableColumnHeaderState();
}
class _EditableColumnHeaderState extends State<EditableColumnHeader> {
late final FocusNode focusNode;
late final TextEditingController textController = TextEditingController(
text: _generateGroupName(),
);
GroupData get customData => widget.groupData.customData;
@override
void initState() {
super.initState();
focusNode = FocusNode(
onKeyEvent: (node, event) {
if (event.logicalKey == LogicalKeyboardKey.escape &&
event is KeyUpEvent) {
focusNode.unfocus();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
)..addListener(() {
if (!focusNode.hasFocus) {
widget.isEditing.value = false;
widget.onSubmitted(textController.text);
} else {
textController.selection = TextSelection(
baseOffset: 0,
extentOffset: textController.text.length,
);
}
});
}
@override
void didUpdateWidget(covariant oldWidget) {
if (oldWidget.groupData.customData != widget.groupData.customData) {
textController.text = _generateGroupName();
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
focusNode.dispose();
textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: ValueListenableBuilder(
valueListenable: widget.isEditing,
builder: (context, isEditing, _) {
if (isEditing) {
focusNode.requestFocus();
}
return isEditing ? _buildTextField() : _buildTitle();
},
),
),
const HSpace(6),
GroupOptionsButton(
groupData: widget.groupData,
isEditing: widget.isEditing,
),
const HSpace(4),
CreateCardFromTopButton(
groupId: widget.groupData.id,
),
],
);
}
Widget _buildTitle() {
final (backgroundColor, dotColor) = _generateGroupColor();
final groupName = _generateGroupName();
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
widget.isEditing.value = true;
},
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowyTooltip(
message: groupName,
child: Container(
height: 20,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
height: 6,
width: 6,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const HSpace(4.0),
Flexible(
child: FlowyText.medium(
groupName,
overflow: TextOverflow.ellipsis,
lineHeight: 1.0,
),
),
],
),
),
),
),
),
);
}
Widget _buildTextField() {
return TextField(
controller: textController,
focusNode: focusNode,
onEditingComplete: () {
widget.isEditing.value = false;
},
onSubmitted: widget.onSubmitted,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
hoverColor: Colors.transparent,
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
isDense: true,
),
);
}
String _generateGroupName() {
return customData.group.generateGroupName(widget.databaseController);
}
(Color? backgroundColor, Color? dotColor) _generateGroupColor() {
Color? backgroundColor;
Color? dotColor;
final groupId = widget.groupData.id;
final fieldId = customData.fieldInfo.id;
final field = widget.databaseController.fieldController.getField(fieldId);
if (field != null) {
final selectOptions = switch (field.fieldType) {
FieldType.MultiSelect => MultiSelectTypeOptionDataParser()
.fromBuffer(field.field.typeOptionData)
.options,
FieldType.SingleSelect => SingleSelectTypeOptionDataParser()
.fromBuffer(field.field.typeOptionData)
.options,
_ => <SelectOptionPB>[],
};
final colorPB =
selectOptions.firstWhereOrNull((e) => e.id == groupId)?.color;
if (colorPB != null) {
backgroundColor = colorPB.toColor(context);
dotColor = getColorOfDot(colorPB);
}
}
return (backgroundColor, dotColor);
}
// move to theme file and allow theme customization once palette is finalized
Color getColorOfDot(SelectOptionColorPB color) {
return switch (Theme.of(context).brightness) {
Brightness.light => switch (color) {
SelectOptionColorPB.Purple => const Color(0xFFAB8DFF),
SelectOptionColorPB.Pink => const Color(0xFFFF8EF5),
SelectOptionColorPB.LightPink => const Color(0xFFFF85A9),
SelectOptionColorPB.Orange => const Color(0xFFFFBC7E),
SelectOptionColorPB.Yellow => const Color(0xFFFCD86F),
SelectOptionColorPB.Lime => const Color(0xFFC6EC41),
SelectOptionColorPB.Green => const Color(0xFF74F37D),
SelectOptionColorPB.Aqua => const Color(0xFF40F0D1),
SelectOptionColorPB.Blue => const Color(0xFF00C8FF),
_ => throw ArgumentError,
},
Brightness.dark => switch (color) {
SelectOptionColorPB.Purple => const Color(0xFF502FD6),
SelectOptionColorPB.Pink => const Color(0xFFBF1CC0),
SelectOptionColorPB.LightPink => const Color(0xFFC42A53),
SelectOptionColorPB.Orange => const Color(0xFFD77922),
SelectOptionColorPB.Yellow => const Color(0xFFC59A1A),
SelectOptionColorPB.Lime => const Color(0xFFA4C824),
SelectOptionColorPB.Green => const Color(0xFF23CA2E),
SelectOptionColorPB.Aqua => const Color(0xFF19CCAC),
SelectOptionColorPB.Blue => const Color(0xFF04A9D7),
_ => throw ArgumentError,
}
};
}
}
@@ -419,12 +419,11 @@ class HiddenGroupPopupItemList extends StatelessWidget {
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (_) {
return RowDetailPage(
databaseController: databaseController,
rowController: rowController,
);
},
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
),
);
PopoverContainer.of(context).close();
},
@@ -7,6 +7,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:fixnum/fixnum.dart';
@@ -33,11 +34,20 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
CellMemCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
void _dispatch() {
on<CalendarEvent>(
(event, emit) async {
await event.when(
initial: () async {
final result = await UserEventGetUserProfile().send();
result.fold(
(profile) => _userProfile = profile,
(err) => Log.error('Failed to get user profile: $err'),
);
_startListening();
await _openDatabase(emit);
_loadAllEvents();
@@ -2,7 +2,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@@ -12,6 +11,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../application/calendar_bloc.dart';
@@ -65,14 +65,14 @@ class CalendarDayCard extends StatelessWidget {
const VSpace(6.0),
// List of cards or empty space
if (events.isNotEmpty && !PlatformExtension.isMobile) ...[
if (events.isNotEmpty && !UniversalPlatform.isMobile) ...[
_EventList(
events: events,
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
),
] else if (events.isNotEmpty && PlatformExtension.isMobile) ...[
] else if (events.isNotEmpty && UniversalPlatform.isMobile) ...[
const _EventIndicator(),
],
],
@@ -82,7 +82,7 @@ class CalendarDayCard extends StatelessWidget {
children: [
GestureDetector(
onDoubleTap: () => onCreateEvent(date),
onTap: PlatformExtension.isMobile
onTap: UniversalPlatform.isMobile
? () => _mobileOnTap(context)
: null,
child: Container(
@@ -106,7 +106,7 @@ class CalendarDayCard extends StatelessWidget {
padding: const EdgeInsets.only(top: 5.0),
child: child,
),
if (candidate.isEmpty && !PlatformExtension.isMobile)
if (candidate.isEmpty && !UniversalPlatform.isMobile)
NewEventButton(
onCreate: () => onCreateEvent(date),
),
@@ -303,16 +303,16 @@ class _DayBadge extends StatelessWidget {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
final double size = PlatformExtension.isMobile ? 20 : 18;
final double size = UniversalPlatform.isMobile ? 20 : 18;
return SizedBox(
height: size,
child: Row(
mainAxisAlignment: PlatformExtension.isMobile
mainAxisAlignment: UniversalPlatform.isMobile
? MainAxisAlignment.center
: MainAxisAlignment.end,
children: [
if (date.day == 1 && !PlatformExtension.isMobile)
if (date.day == 1 && !UniversalPlatform.isMobile)
FlowyText.medium(
monthString,
fontSize: 11,
@@ -328,7 +328,7 @@ class _DayBadge extends StatelessWidget {
child: Center(
child: FlowyText.medium(
dayString,
fontSize: PlatformExtension.isMobile ? 12 : 11,
fontSize: UniversalPlatform.isMobile ? 12 : 11,
color: dayTextColor,
),
),
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
@@ -5,17 +7,17 @@ import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.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 'package:go_router/go_router.dart';
import 'package:universal_platform/universal_platform.dart';
import '../application/calendar_bloc.dart';
import 'calendar_event_editor.dart';
class EventCard extends StatefulWidget {
@@ -80,8 +82,9 @@ class _EventCardState extends State<EventCard> {
rowCache: rowCache,
isEditing: false,
cellBuilder: cellBuilder,
isCompact: true,
onTap: (context) {
if (PlatformExtension.isMobile) {
if (UniversalPlatform.isMobile) {
context.push(
MobileRowDetailPage.routeName,
extra: {
@@ -107,6 +110,7 @@ class _EventCardState extends State<EventCard> {
),
onStartEditing: () {},
onEndEditing: () {},
userProfile: context.read<CalendarBloc>().userProfile,
);
final decoration = BoxDecoration(
@@ -167,12 +171,14 @@ class _EventCardState extends State<EventCard> {
),
);
},
child: Material(
color: Colors.transparent,
child: Container(
padding: widget.padding,
decoration: decoration,
child: card,
child: Padding(
padding: widget.padding,
child: Material(
color: Colors.transparent,
child: DecoratedBox(
decoration: decoration,
child: card,
),
),
),
);
@@ -130,6 +130,7 @@ class EventEditorControls extends StatelessWidget {
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<CalendarBloc>().userProfile,
),
),
);
@@ -287,10 +288,8 @@ class _TitleTextCellSkin extends IEditableTextCellSkin {
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14),
focusNode: focusNode,
hintText: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
onChanged: (text) {
if (textEditingController.value.composing.isCollapsed) {
bloc.add(TextCellEvent.updateText(text));
}
onEditingComplete: () {
bloc.add(TextCellEvent.updateText(textEditingController.text));
},
);
}
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@@ -11,19 +13,19 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.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';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../application/row/row_controller.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_setting_bar.dart';
@@ -183,7 +185,7 @@ class _CalendarPageState extends State<CalendarPage> {
return LayoutBuilder(
// must specify MonthView width for useAvailableVerticalSpace to work properly
builder: (context, constraints) {
EdgeInsets padding = PlatformExtension.isMobile
EdgeInsets padding = UniversalPlatform.isMobile
? CalendarSize.contentInsetsMobile
: CalendarSize.contentInsets +
const EdgeInsets.symmetric(horizontal: 40);
@@ -201,7 +203,7 @@ class _CalendarPageState extends State<CalendarPage> {
key: _calendarState,
controller: _eventController,
width: constraints.maxWidth,
cellAspectRatio: PlatformExtension.isMobile ? 0.9 : 0.6,
cellAspectRatio: UniversalPlatform.isMobile ? 0.9 : 0.6,
startDay: _weekdayFromInt(firstDayOfWeek),
showBorder: false,
headerBuilder: _headerNavigatorBuilder,
@@ -221,7 +223,7 @@ class _CalendarPageState extends State<CalendarPage> {
child: Row(
children: [
GestureDetector(
onTap: PlatformExtension.isMobile
onTap: UniversalPlatform.isMobile
? () => showMobileBottomSheet(
context,
title: LocaleKeys.calendar_quickJumpYear.tr(),
@@ -248,7 +250,7 @@ class _CalendarPageState extends State<CalendarPage> {
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
if (PlatformExtension.isMobile) ...[
if (UniversalPlatform.isMobile) ...[
const HSpace(6),
const FlowySvg(FlowySvgs.arrow_down_s),
],
@@ -298,7 +300,7 @@ class _CalendarPageState extends State<CalendarPage> {
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
String weekDayString = symbols.WEEKDAYS[(day + 1) % 7];
if (PlatformExtension.isMobile) {
if (UniversalPlatform.isMobile) {
weekDayString = weekDayString.substring(0, 3);
}
@@ -365,6 +367,7 @@ void showEventDetails({
child: RowDetailPage(
rowController: rowController,
databaseController: databaseController,
userProfile: context.read<CalendarBloc>().userProfile,
),
);
},
@@ -410,7 +413,7 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
),
onPressed: () {
if (state.unscheduleEvents.isNotEmpty) {
if (PlatformExtension.isMobile) {
if (UniversalPlatform.isMobile) {
_showUnscheduledEventsMobile(state.unscheduleEvents);
} else {
_popoverController.show();
@@ -428,15 +431,16 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
),
),
),
popupBuilder: (_) {
return BlocProvider.value(
popupBuilder: (_) => BlocProvider.value(
value: context.read<CalendarBloc>(),
child: BlocProvider.value(
value: context.read<ViewBloc>(),
child: UnscheduleEventsList(
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
),
);
},
),
),
);
},
),
@@ -475,7 +479,7 @@ class UnscheduleEventsList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cells = [
if (!PlatformExtension.isMobile)
if (!UniversalPlatform.isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
@@ -489,7 +493,7 @@ class UnscheduleEventsList extends StatelessWidget {
(event) => UnscheduledEventCell(
event: event,
onPressed: () {
if (PlatformExtension.isMobile) {
if (UniversalPlatform.isMobile) {
context.push(
MobileRowDetailPage.routeName,
extra: {
@@ -519,7 +523,7 @@ class UnscheduleEventsList extends StatelessWidget {
shrinkWrap: true,
);
if (PlatformExtension.isMobile) {
if (UniversalPlatform.isMobile) {
return Flexible(child: child);
}
@@ -539,7 +543,7 @@ class UnscheduledEventCell extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PlatformExtension.isMobile
return UniversalPlatform.isMobile
? MobileUnscheduledEventTile(event: event, onPressed: onPressed)
: DesktopUnscheduledEventTile(event: event, onPressed: onPressed);
}
@@ -22,12 +22,10 @@ class GroupBackendService {
Future<FlowyResult<void, FlowyError>> updateGroup({
required String groupId,
required String fieldId,
String? name,
bool? visible,
}) {
final payload = UpdateGroupPB.create()
..fieldId = fieldId
..viewId = viewId
..groupId = groupId;
@@ -60,18 +58,4 @@ class GroupBackendService {
return DatabaseEventDeleteGroup(payload).send();
}
Future<FlowyResult<void, FlowyError>> renameGroup({
required String groupId,
required String fieldId,
required String name,
}) {
final payload = RenameGroupPB.create()
..fieldId = fieldId
..viewId = viewId
..groupId = groupId
..name = name;
return DatabaseEventRenameGroup(payload).send();
}
}
@@ -6,9 +6,11 @@ import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -27,11 +29,20 @@ class GridBloc extends Bloc<GridEvent, GridState> {
String get viewId => databaseController.viewId;
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
void _dispatch() {
on<GridEvent>(
(event, emit) async {
await event.when(
initial: () async {
final response = await UserEventGetUserProfile().send();
response.fold(
(userProfile) => _userProfile = userProfile,
(err) => Log.error(err),
);
_startListening();
await _openGrid(emit);
},
@@ -1,5 +1,10 @@
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -9,17 +14,23 @@ class MobileRowDetailBloc
extends Bloc<MobileRowDetailEvent, MobileRowDetailState> {
MobileRowDetailBloc({required this.databaseController})
: super(MobileRowDetailState.initial()) {
rowBackendService = RowBackendService(viewId: databaseController.viewId);
_dispatch();
}
final DatabaseController databaseController;
late final RowBackendService rowBackendService;
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
void _dispatch() {
on<MobileRowDetailEvent>(
(event, emit) {
event.when(
initial: (rowId) {
initial: (rowId) async {
_startListening();
emit(
state.copyWith(
isLoading: false,
@@ -27,6 +38,12 @@ class MobileRowDetailBloc
rowInfos: databaseController.rowCache.rowInfos,
),
);
final result = await UserEventGetUserProfile().send();
result.fold(
(profile) => _userProfile = profile,
(error) => Log.error(error),
);
},
didLoadRows: (rows) {
emit(state.copyWith(rowInfos: rows));
@@ -34,6 +51,16 @@ class MobileRowDetailBloc
changeRowId: (rowId) {
emit(state.copyWith(currentRowId: rowId));
},
addCover: (rowCover) async {
if (state.currentRowId == null) {
return;
}
await rowBackendService.updateMeta(
rowId: state.currentRowId!,
cover: rowCover,
);
},
);
},
);
@@ -66,6 +93,7 @@ class MobileRowDetailEvent with _$MobileRowDetailEvent {
const factory MobileRowDetailEvent.didLoadRows(List<RowInfo> rows) =
_DidLoadRows;
const factory MobileRowDetailEvent.changeRowId(String rowId) = _ChangeRowId;
const factory MobileRowDetailEvent.addCover(RowCoverPB cover) = _AddCover;
}
@freezed
@@ -1,12 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/domain/field_settings_service.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/domain/row_meta_listener.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -16,7 +21,9 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
RowDetailBloc({
required this.fieldController,
required this.rowController,
}) : super(RowDetailState.initial()) {
}) : _metaListener = RowMetaListener(rowController.rowId),
_rowService = RowBackendService(viewId: rowController.viewId),
super(RowDetailState.initial(rowController.rowMeta)) {
_dispatch();
_startListening();
_init();
@@ -26,12 +33,14 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
final FieldController fieldController;
final RowController rowController;
final RowMetaListener _metaListener;
final RowBackendService _rowService;
final List<CellContext> allCells = [];
@override
Future<void> close() async {
await rowController.dispose();
await _metaListener.stop();
return super.close();
}
@@ -91,12 +100,26 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
endEditingField: () {
emit(state.copyWith(editingFieldId: "", newFieldId: ""));
},
removeCover: () => _rowService.removeCover(rowController.rowId),
setCover: (cover) =>
_rowService.updateMeta(rowId: rowController.rowId, cover: cover),
didReceiveRowMeta: (rowMeta) {
emit(state.copyWith(rowMeta: rowMeta));
},
);
},
);
}
void _startListening() {
_metaListener.start(
callback: (rowMeta) {
if (!isClosed) {
add(RowDetailEvent.didReceiveRowMeta(rowMeta));
}
},
);
rowController.addListener(
onRowChanged: (cellMap, reason) {
if (isClosed) {
@@ -238,6 +261,13 @@ class RowDetailEvent with _$RowDetailEvent {
/// End editing an event
const factory RowDetailEvent.endEditingField() = _EndEditingField;
const factory RowDetailEvent.removeCover() = _RemoveCover;
const factory RowDetailEvent.setCover(RowCoverPB cover) = _SetCover;
const factory RowDetailEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
_DidReceiveRowMeta;
}
@freezed
@@ -249,14 +279,16 @@ class RowDetailState with _$RowDetailState {
required int numHiddenFields,
required String editingFieldId,
required String newFieldId,
required RowMetaPB rowMeta,
}) = _RowDetailState;
factory RowDetailState.initial() => const RowDetailState(
factory RowDetailState.initial(RowMetaPB rowMeta) => RowDetailState(
fields: [],
visibleCells: [],
showHiddenFields: false,
numHiddenFields: 0,
editingFieldId: "",
newFieldId: "",
rowMeta: rowMeta,
);
}
@@ -81,7 +81,7 @@ class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {
// new document for the given document id of the row.
final documentView =
await _createRowDocumentView(rowMeta.documentId);
if (documentView != null) {
if (documentView != null && !isClosed) {
add(RowDocumentEvent.didReceiveRowDocument(documentView));
}
} else {
@@ -1,6 +1,10 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
@@ -9,11 +13,12 @@ import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import 'package:provider/provider.dart';
@@ -23,6 +28,7 @@ import '../../application/row/row_controller.dart';
import '../../tab_bar/tab_bar_view.dart';
import '../../widgets/row/row_detail.dart';
import '../application/grid_bloc.dart';
import 'grid_scroll.dart';
import 'layout/layout.dart';
import 'layout/sizes.dart';
@@ -197,6 +203,7 @@ class _GridPageState extends State<GridPage> {
child: RowDetailPage(
databaseController: context.read<GridBloc>().databaseController,
rowController: rowController,
userProfile: context.read<GridBloc>().userProfile,
),
),
);
@@ -293,12 +300,14 @@ class _GridRowsState extends State<_GridRows> {
void _evaluateFloatingCalculations() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
// maxScrollExtent is 0.0 if scrolling is not possible
showFloatingCalculations = widget
.scrollController.verticalController.position.maxScrollExtent >
0;
});
if (mounted) {
setState(() {
// maxScrollExtent is 0.0 if scrolling is not possible
showFloatingCalculations = widget.scrollController.verticalController
.position.maxScrollExtent >
0;
});
}
});
}
@@ -349,7 +358,7 @@ class _GridRowsState extends State<_GridRows> {
child: ReorderableListView.builder(
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: max(layoutConstraints.maxHeight * 2, 500),
cacheExtent: max(layoutConstraints.maxHeight, 500),
scrollController: widget.scrollController.verticalController,
physics: const ClampingScrollPhysics(),
buildDefaultDragHandles: false,
@@ -361,11 +370,34 @@ class _GridRowsState extends State<_GridRows> {
),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex != toIndex) {
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
void moveRow() {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex != toIndex) {
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
}
}
if (state.sorts.isNotEmpty) {
showCancelAndDeleteDialog(
context: context,
title: LocaleKeys.grid_sort_sortsActive.tr(
namedArgs: {
'intention':
LocaleKeys.grid_row_reorderRowDescription.tr(),
},
),
description: LocaleKeys.grid_sort_removeSorting.tr(),
confirmLabel: LocaleKeys.button_remove.tr(),
closeOnAction: true,
onDelete: () {
SortBackendService(viewId: widget.viewId).deleteAllSorts();
moveRow();
},
);
} else {
moveRow();
}
},
itemCount: itemCount,
@@ -374,7 +406,6 @@ class _GridRowsState extends State<_GridRows> {
return _renderRow(
context,
state.rowInfos[index].rowId,
isDraggable: state.reorderable,
index: index,
);
}
@@ -411,8 +442,7 @@ class _GridRowsState extends State<_GridRows> {
Widget _renderRow(
BuildContext context,
RowId rowId, {
int? index,
required bool isDraggable,
required int index,
Animation<double>? animation,
}) {
final databaseController = context.read<GridBloc>().databaseController;
@@ -424,11 +454,6 @@ class _GridRowsState extends State<_GridRows> {
Log.warn('RowMeta is null for rowId: $rowId');
return const SizedBox.shrink();
}
final rowController = RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
);
final child = GridRow(
key: ValueKey(rowId),
@@ -436,21 +461,32 @@ class _GridRowsState extends State<_GridRows> {
rowId: rowId,
viewId: viewId,
index: index,
isDraggable: isDraggable,
rowController: rowController,
rowController: RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
),
cellBuilder: EditableCellBuilder(databaseController: databaseController),
openDetailPage: (rowDetailContext) {
FlowyOverlay.show(
context: rowDetailContext,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
rowController: rowController,
databaseController: databaseController,
),
),
);
},
openDetailPage: (rowDetailContext) => FlowyOverlay.show(
context: rowDetailContext,
builder: (_) {
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
return rowMeta == null
? const SizedBox.shrink()
: BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
rowController: RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
),
databaseController: databaseController,
userProfile: context.read<GridBloc>().userProfile,
),
);
},
),
);
if (animation != null) {
@@ -1,5 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/widgets.dart';
import 'package:universal_platform/universal_platform.dart';
class GridSize {
static double scale = 1;
@@ -9,7 +9,7 @@ class GridSize {
static double get buttonHeight => 38 * scale;
static double get footerHeight => 40 * scale;
static double get horizontalHeaderPadding =>
PlatformExtension.isDesktop ? 40 * scale : 16 * scale;
UniversalPlatform.isDesktop ? 40 * scale : 16 * scale;
static double get trailHeaderPadding => 140 * scale;
static double get cellHPadding => 10 * scale;
static double get cellVPadding => 10 * scale;
@@ -36,8 +36,8 @@ class GridSize {
static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB(
GridSize.horizontalHeaderPadding,
0,
PlatformExtension.isMobile ? GridSize.horizontalHeaderPadding : 0,
PlatformExtension.isMobile ? 100 : 0,
UniversalPlatform.isMobile ? GridSize.horizontalHeaderPadding : 0,
UniversalPlatform.isMobile ? 100 : 0,
);
static EdgeInsets get contentInsets => EdgeInsets.symmetric(
@@ -6,7 +6,6 @@ import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -14,6 +13,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reorderables/reorderables.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../layout/sizes.dart';
import 'desktop_field_cell.dart';
@@ -112,7 +112,7 @@ class _GridHeaderState extends State<_GridHeader> {
),
draggingWidgetOpacity: 0,
header: _cellLeading(),
needsLongPressDraggable: PlatformExtension.isMobile,
needsLongPressDraggable: UniversalPlatform.isMobile,
footer: _CellTrailing(viewId: widget.viewId),
onReorder: (int oldIndex, int newIndex) {
context
@@ -1,6 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@@ -8,6 +10,7 @@ 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';
class RowActionMenu extends StatelessWidget {
const RowActionMenu({
@@ -57,17 +60,7 @@ class RowActionMenu extends StatelessWidget {
lineHeight: 1.0,
),
onTap: () {
if (action == RowAction.delete) {
NavigatorOkCancelDialog(
message: LocaleKeys.grid_row_deleteRowPrompt.tr(),
onOkPressed: () {
action.performAction(context, viewId, rowId);
},
).show(context);
} else {
action.performAction(context, viewId, rowId);
}
action.performAction(context, viewId, rowId);
PopoverContainer.of(context).close();
},
leftIcon: icon,
@@ -107,17 +100,45 @@ enum RowAction {
final position = this == insertAbove
? OrderObjectPositionTypePB.Before
: OrderObjectPositionTypePB.After;
RowBackendService.createRow(
viewId: viewId,
position: position,
targetRowId: rowId,
);
final intention = this == insertAbove
? LocaleKeys.grid_row_createRowAboveDescription.tr()
: LocaleKeys.grid_row_createRowBelowDescription.tr();
if (context.read<GridBloc>().state.sorts.isNotEmpty) {
showCancelAndDeleteDialog(
context: context,
title: LocaleKeys.grid_sort_sortsActive.tr(
namedArgs: {'intention': intention},
),
description: LocaleKeys.grid_sort_removeSorting.tr(),
confirmLabel: LocaleKeys.button_remove.tr(),
closeOnAction: true,
onDelete: () {
SortBackendService(viewId: viewId).deleteAllSorts();
RowBackendService.createRow(
viewId: viewId,
position: position,
targetRowId: rowId,
);
},
);
} else {
RowBackendService.createRow(
viewId: viewId,
position: position,
targetRowId: rowId,
);
}
break;
case duplicate:
RowBackendService.duplicateRow(viewId, rowId);
break;
case delete:
RowBackendService.deleteRows(viewId, [rowId]);
showConfirmDeletionDialog(
context: context,
name: LocaleKeys.grid_row_label.tr(),
description: LocaleKeys.grid_row_deleteRowPrompt.tr(),
onConfirm: () => RowBackendService.deleteRows(viewId, [rowId]),
);
break;
}
}
@@ -1,3 +1,6 @@
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -31,8 +34,7 @@ class GridRow extends StatefulWidget {
required this.rowController,
required this.cellBuilder,
required this.openDetailPage,
this.index,
this.isDraggable = false,
required this.index,
});
final FieldController fieldController;
@@ -41,8 +43,7 @@ class GridRow extends StatefulWidget {
final RowController rowController;
final EditableCellBuilder cellBuilder;
final void Function(BuildContext context) openDetailPage;
final int? index;
final bool isDraggable;
final int index;
@override
State<GridRow> createState() => _GridRowState();
@@ -62,8 +63,8 @@ class _GridRowState extends State<GridRow> {
child: Row(
children: [
_RowLeading(
viewId: widget.viewId,
index: widget.index,
isDraggable: widget.isDraggable,
),
Expanded(
child: RowContent(
@@ -81,12 +82,12 @@ class _GridRowState extends State<GridRow> {
class _RowLeading extends StatefulWidget {
const _RowLeading({
this.index,
this.isDraggable = false,
required this.viewId,
required this.index,
});
final int? index;
final bool isDraggable;
final String viewId;
final int index;
@override
State<_RowLeading> createState() => _RowLeadingState();
@@ -105,9 +106,12 @@ class _RowLeadingState extends State<_RowLeading> {
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
popupBuilder: (_) {
final bloc = context.read<RowBloc>();
return RowActionMenu(
viewId: bloc.viewId,
rowId: bloc.rowId,
return BlocProvider.value(
value: context.read<GridBloc>(),
child: RowActionMenu(
viewId: bloc.viewId,
rowId: bloc.rowId,
),
);
},
child: Consumer<RegionStateNotifier>(
@@ -127,28 +131,25 @@ class _RowLeadingState extends State<_RowLeading> {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const InsertRowButton(),
if (isDraggable)
ReorderableDragStartListener(
index: widget.index!,
child: RowMenuButton(
isDragEnabled: isDraggable,
openMenu: popoverController.show,
),
)
else
RowMenuButton(
InsertRowButton(viewId: widget.viewId),
ReorderableDragStartListener(
index: widget.index,
child: RowMenuButton(
openMenu: popoverController.show,
),
),
],
);
}
bool get isDraggable => widget.index != null && widget.isDraggable;
}
class InsertRowButton extends StatelessWidget {
const InsertRowButton({super.key});
const InsertRowButton({
super.key,
required this.viewId,
});
final String viewId;
@override
Widget build(BuildContext context) {
@@ -157,7 +158,28 @@ class InsertRowButton extends StatelessWidget {
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 20,
height: 30,
onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
onPressed: () {
final rowBloc = context.read<RowBloc>();
if (context.read<GridBloc>().state.sorts.isNotEmpty) {
showCancelAndDeleteDialog(
context: context,
title: LocaleKeys.grid_sort_sortsActive.tr(
namedArgs: {
'intention': LocaleKeys.grid_row_createRowBelowDescription.tr(),
},
),
description: LocaleKeys.grid_sort_removeSorting.tr(),
confirmLabel: LocaleKeys.button_remove.tr(),
closeOnAction: true,
onDelete: () {
SortBackendService(viewId: viewId).deleteAllSorts();
rowBloc.add(const RowEvent.createRow());
},
);
} else {
rowBloc.add(const RowEvent.createRow());
}
},
iconPadding: const EdgeInsets.all(3),
icon: FlowySvg(
FlowySvgs.add_s,
@@ -171,11 +193,9 @@ class RowMenuButton extends StatefulWidget {
const RowMenuButton({
super.key,
required this.openMenu,
this.isDragEnabled = false,
});
final VoidCallback openMenu;
final bool isDragEnabled;
@override
State<RowMenuButton> createState() => _RowMenuButtonState();
@@ -185,22 +205,18 @@ class _RowMenuButtonState extends State<RowMenuButton> {
@override
Widget build(BuildContext context) {
return FlowyIconButton(
tooltipText:
widget.isDragEnabled ? null : LocaleKeys.tooltip_openMenu.tr(),
richTooltipText: widget.isDragEnabled
? TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.tooltip_dragRow.tr()}\n',
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.tooltip_openMenu.tr(),
style: context.tooltipTextStyle(),
),
],
)
: null,
richTooltipText: TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.tooltip_dragRow.tr()}\n',
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.tooltip_openMenu.tr(),
style: context.tooltipTextStyle(),
),
],
),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 20,
height: 30,
@@ -2,7 +2,6 @@ import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/grid/application/grid_accessory_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_menu.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -55,27 +54,22 @@ class _DatabaseViewSettingContent extends StatelessWidget {
return BlocBuilder<DatabaseViewSettingExtensionBloc,
DatabaseViewSettingExtensionState>(
builder: (context, state) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.horizontalHeaderPadding,
),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
),
return DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
SortMenu(fieldController: fieldController),
const HSpace(6),
FilterMenu(fieldController: fieldController),
],
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
SortMenu(fieldController: fieldController),
const HSpace(6),
FilterMenu(fieldController: fieldController),
],
),
),
);
@@ -12,12 +12,12 @@ import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_v
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:universal_platform/universal_platform.dart';
import 'desktop/tab_bar_header.dart';
import 'mobile/mobile_tab_bar_header.dart';
@@ -108,7 +108,7 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
],
child: Column(
children: [
if (PlatformExtension.isMobile) const VSpace(12),
if (UniversalPlatform.isMobile) const VSpace(12),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return ValueListenableBuilder<bool>(
@@ -121,7 +121,7 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
return const SizedBox.shrink();
}
return PlatformExtension.isDesktop
return UniversalPlatform.isDesktop
? const TabBarHeader()
: const MobileTabBarHeader();
},
@@ -170,9 +170,15 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
final tabBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tabBar.builder.settingBarExtension(
context,
controller,
return Padding(
padding: EdgeInsets.symmetric(
horizontal:
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
),
child: tabBar.builder.settingBarExtension(
context,
controller,
),
);
}
}
@@ -1,21 +1,24 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
import '../cell/card_cell_builder.dart';
import '../cell/card_cell_skeleton/card_cell.dart';
@@ -41,6 +44,8 @@ class RowCard extends StatefulWidget {
this.onShiftTap,
this.groupingFieldId,
this.groupId,
required this.userProfile,
this.isCompact = false,
});
final FieldController fieldController;
@@ -68,6 +73,14 @@ class RowCard extends StatefulWidget {
final RowCardStyleConfiguration styleConfiguration;
/// Specifically the token is used to handle requests to retrieve images
/// from cloud storage, such as the card cover.
final UserProfilePB? userProfile;
/// Whether the card is in a narrow space.
/// This is used to determine eg. the Cover height.
final bool isCompact;
@override
State<RowCard> createState() => _RowCardState();
}
@@ -121,7 +134,7 @@ class _RowCardState extends State<RowCard> {
}
},
builder: (context, state) =>
PlatformExtension.isMobile ? _mobile(state) : _desktop(state),
UniversalPlatform.isMobile ? _mobile(state) : _desktop(state),
),
);
}
@@ -131,6 +144,7 @@ class _RowCardState extends State<RowCard> {
onTap: () => widget.onTap(context),
behavior: HitTestBehavior.opaque,
child: MobileCardContent(
userProfile: widget.userProfile,
rowMeta: state.rowMeta,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
@@ -167,6 +181,8 @@ class _RowCardState extends State<RowCard> {
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
userProfile: widget.userProfile,
isCompact: widget.isCompact,
),
),
);
@@ -190,48 +206,35 @@ class _CardContent extends StatelessWidget {
required this.cellBuilder,
required this.cells,
required this.styleConfiguration,
this.userProfile,
this.isCompact = false,
});
final RowMetaPB rowMeta;
final CardCellBuilder cellBuilder;
final List<CellMeta> cells;
final RowCardStyleConfiguration styleConfiguration;
final UserProfilePB? userProfile;
final bool isCompact;
@override
Widget build(BuildContext context) {
final attachmentCount = rowMeta.attachmentCount.toInt();
final child = Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
..._makeCells(context, rowMeta, cells),
if (attachmentCount > 0) ...[
const VSpace(2),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Row(
children: [
const FlowySvg(
FlowySvgs.media_s,
size: Size.square(12),
),
const HSpace(4),
Flexible(
child: FlowyText.regular(
LocaleKeys.grid_media_attachmentsHint
.tr(args: ['$attachmentCount']),
fontSize: 11,
color: AFThemeExtension.of(context).secondaryTextColor,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
],
),
final child = Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
CardCover(
cover: rowMeta.cover,
userProfile: userProfile,
isCompact: isCompact,
),
Padding(
padding: styleConfiguration.cardPadding,
child: Column(
children: _makeCells(context, rowMeta, cells),
),
),
],
);
return styleConfiguration.hoverStyle == null
? child
@@ -269,6 +272,94 @@ class _CardContent extends StatelessWidget {
}
}
class CardCover extends StatelessWidget {
const CardCover({
super.key,
this.cover,
this.userProfile,
this.isCompact = false,
});
final RowCoverPB? cover;
final UserProfilePB? userProfile;
final bool isCompact;
@override
Widget build(BuildContext context) {
if (cover == null ||
cover!.data.isEmpty ||
cover!.uploadType == FileUploadTypePB.CloudFile &&
userProfile == null) {
return const SizedBox.shrink();
}
return Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
color: Theme.of(context).cardColor,
),
child: Row(
children: [
Expanded(child: _renderCover(context, cover!)),
],
),
);
}
Widget _renderCover(BuildContext context, RowCoverPB cover) {
final height = isCompact ? 50.0 : 100.0;
if (cover.coverType == CoverTypePB.FileCover) {
return SizedBox(
height: height,
width: double.infinity,
child: AFImage(
url: cover.data,
uploadType: cover.uploadType,
userProfile: userProfile,
),
);
}
if (cover.coverType == CoverTypePB.AssetCover) {
return SizedBox(
height: height,
width: double.infinity,
child: Image.asset(
PageStyleCoverImageType.builtInImagePath(cover.data),
fit: BoxFit.cover,
),
);
}
if (cover.coverType == CoverTypePB.ColorCover) {
final color = FlowyTint.fromId(cover.data)?.color(context) ??
cover.data.tryToColor();
return Container(
height: height,
width: double.infinity,
color: color,
);
}
if (cover.coverType == CoverTypePB.GradientCover) {
return Container(
height: height,
width: double.infinity,
decoration: BoxDecoration(
gradient: FlowyGradientColor.fromId(cover.data).linear,
),
);
}
return const SizedBox.shrink();
}
}
class EditCardAccessory extends StatelessWidget with CardAccessory {
const EditCardAccessory({super.key});
@@ -28,7 +28,7 @@ class RowCardContainer extends StatelessWidget {
create: (_) => _CardContainerNotifier(),
child: Consumer<_CardContainerNotifier>(
builder: (context, notifier, _) {
Widget container = Center(child: child);
Widget container = child;
bool shouldBuildAccessory = true;
if (buildAccessoryWhen != null) {
shouldBuildAccessory = buildAccessoryWhen!.call();
@@ -52,7 +52,7 @@ class RowCardContainer extends StatelessWidget {
}
},
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
constraints: const BoxConstraints(minHeight: 42),
child: container,
),
);
@@ -1,8 +1,17 @@
import 'package:flutter/widgets.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/card_cell.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MediaCardCellStyle extends CardCellStyle {
const MediaCardCellStyle({
@@ -13,9 +22,6 @@ class MediaCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
}
// This is a placeholder for the MediaCardCell, it is not implemented
// as we use the [RowMetaPB.attachmentCount] to display cumulative attachments
// on a Card.
class MediaCardCell extends CardCell<MediaCardCellStyle> {
const MediaCardCell({
super.key,
@@ -34,6 +40,42 @@ class MediaCardCell extends CardCell<MediaCardCellStyle> {
class _MediaCellState extends State<MediaCardCell> {
@override
Widget build(BuildContext context) {
return const SizedBox.shrink();
return BlocProvider<MediaCellBloc>(
create: (_) => MediaCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const MediaCellEvent.initial()),
child: BlocBuilder<MediaCellBloc, MediaCellState>(
builder: (context, state) {
if (state.files.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(left: 4),
child: Row(
children: [
const FlowySvg(
FlowySvgs.media_s,
size: Size.square(12),
),
const HSpace(6),
Flexible(
child: FlowyText.regular(
LocaleKeys.grid_media_attachmentsHint
.tr(args: ['${state.files.length}']),
fontSize: 12,
color: AFThemeExtension.of(context).secondaryTextColor,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
},
),
);
}
}
@@ -122,9 +122,7 @@ class _TextCellState extends State<TextCardCell> {
child: BlocListener<TextCellBloc, TextCellState>(
listenWhen: (previous, current) => previous.content != current.content,
listener: (context, state) {
if (!state.enableEdit) {
_textEditingController.text = state.content;
}
_textEditingController.text = state.content ?? "";
},
child: isTitle ? _buildTitle() : _buildText(),
),
@@ -143,9 +141,9 @@ class _TextCellState extends State<TextCardCell> {
Widget? _buildIcon(TextCellState state) {
if (state.emoji?.value.isNotEmpty ?? false) {
return Text(
return FlowyText.emoji(
optimizeEmojiAlign: true,
state.emoji?.value ?? '',
style: widget.style.titleTextStyle,
);
}
@@ -164,7 +162,7 @@ class _TextCellState extends State<TextCardCell> {
Widget _buildText() {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final content = state.content;
final content = state.content ?? "";
return content.isEmpty
? const SizedBox.shrink()
@@ -187,6 +185,7 @@ class _TextCellState extends State<TextCardCell> {
builder: (context, state) {
final icon = _buildIcon(state);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null) ...[
icon,
@@ -224,8 +223,7 @@ class _TextCellState extends State<TextCardCell> {
enableInteractiveSelection: isEditing,
style: widget.style.titleTextStyle,
decoration: InputDecoration(
contentPadding: widget.style.padding
.add(const EdgeInsets.symmetric(vertical: 4.0)),
contentPadding: widget.style.padding,
border: InputBorder.none,
enabledBorder: InputBorder.none,
isDense: true,
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -11,14 +9,14 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.
import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/platform_extension.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
class GridMediaCellSkin extends IEditableMediaCellSkin {
const GridMediaCellSkin({this.isMobileRowDetail = false});
@@ -35,7 +33,7 @@ class GridMediaCellSkin extends IEditableMediaCellSkin {
PopoverController popoverController,
MediaCellBloc bloc,
) {
final isMobile = PlatformExtension.isMobile;
final isMobile = UniversalPlatform.isMobile;
Widget child = BlocBuilder<MediaCellBloc, MediaCellState>(
builder: (context, state) {
@@ -48,7 +46,7 @@ class GridMediaCellSkin extends IEditableMediaCellSkin {
if (extraCount > 0) _ExtraInfo(extraCount: extraCount),
];
if (filesToDisplay.isEmpty && isMobile) {
if (isMobileRowDetail && filesToDisplay.isEmpty) {
children.add(
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
@@ -131,7 +129,8 @@ class GridMediaCellSkin extends IEditableMediaCellSkin {
}
child = InkWell(
borderRadius: BorderRadius.circular(12),
borderRadius:
isMobileRowDetail ? BorderRadius.circular(12) : BorderRadius.zero,
onTap: () {
showMobileBottomSheet(
context,
@@ -160,38 +159,16 @@ class _FilePreviewRender extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget child;
if (file.fileType == MediaFileTypePB.Image) {
if (file.uploadType == MediaUploadTypePB.NetworkMedia) {
child = Image.network(
file.url,
height: 32,
width: 32,
fit: BoxFit.cover,
);
} else if (file.uploadType == MediaUploadTypePB.LocalMedia) {
child = Image.file(
File(file.url),
height: 32,
width: 32,
fit: BoxFit.cover,
);
} else {
// Cloud
child = FlowyNetworkImage(
url: file.url,
userProfilePB: context.read<MediaCellBloc>().state.userProfile,
height: 32,
width: 32,
);
}
} else {
child = Container(
if (file.fileType != MediaFileTypePB.Image) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
height: 32,
width: 32,
clipBehavior: Clip.antiAlias,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AFThemeExtension.of(context).greyHover,
borderRadius: BorderRadius.circular(4),
),
child: FlowySvg(
file.fileType.icon,
@@ -201,12 +178,18 @@ class _FilePreviewRender extends StatelessWidget {
}
return Container(
margin: const EdgeInsets.all(2),
margin: const EdgeInsets.symmetric(horizontal: 2),
height: 32,
width: 32,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
child: child,
child: AFImage(
url: file.url,
uploadType: file.uploadType,
userProfile: context.read<MediaCellBloc>().state.userProfile,
),
);
}
}
@@ -20,6 +20,7 @@ class DesktopGridTextCellSkin extends IEditableTextCellSkin {
return Padding(
padding: GridSize.cellContentInsets,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _IconOrEmoji(),
Expanded(
@@ -29,6 +30,7 @@ class DesktopGridTextCellSkin extends IEditableTextCellSkin {
maxLines: context.watch<TextCellBloc>().state.wrap ? null : 1,
style: Theme.of(context).textTheme.bodyMedium,
decoration: const InputDecoration(
contentPadding: EdgeInsets.only(top: 4),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
@@ -52,39 +54,38 @@ class _IconOrEmoji extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (state.emoji != null)
ValueListenableBuilder<String>(
valueListenable: state.emoji!,
builder: (context, value, child) {
if (value.isEmpty) {
return const SizedBox.shrink();
} else {
return FlowyText(
value,
fontSize: 16,
);
}
},
),
if (state.hasDocument != null)
ValueListenableBuilder<bool>(
valueListenable: state.hasDocument!,
builder: (context, hasDocument, child) {
if ((state.emoji?.value.isEmpty ?? true) && hasDocument) {
return FlowySvg(
FlowySvgs.notes_s,
color: Theme.of(context).hintColor,
);
} else {
return const SizedBox.shrink();
}
},
),
const HSpace(6),
],
// if not a title cell, return empty widget
if (state.emoji == null || state.hasDocument == null) {
return const SizedBox.shrink();
}
return ValueListenableBuilder<String>(
valueListenable: state.emoji!,
builder: (context, emoji, _) {
return emoji.isNotEmpty
? Padding(
padding: const EdgeInsetsDirectional.only(end: 6.0),
child: FlowyText.emoji(
optimizeEmojiAlign: true,
emoji,
),
)
: ValueListenableBuilder<bool>(
valueListenable: state.hasDocument!,
builder: (context, hasDocument, _) {
return hasDocument
? Padding(
padding:
const EdgeInsetsDirectional.only(end: 6.0),
child: FlowySvg(
FlowySvgs.notes_s,
color: Theme.of(context).hintColor,
),
)
: const SizedBox.shrink();
},
);
},
);
},
);
@@ -1,10 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';
@@ -13,14 +12,15 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/util/xfile_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@@ -33,11 +33,9 @@ const _defaultFilesToDisplay = 5;
class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
final mutex = PopoverMutex();
final addFileController = PopoverController();
@override
void dispose() {
addFileController.close();
mutex.dispose();
}
@@ -57,6 +55,9 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
? state.files
: state.files.take(_defaultFilesToDisplay).toList();
final extraCount = state.files.length - _defaultFilesToDisplay;
final images = state.files
.where((f) => f.fileType == MediaFileTypePB.Image)
.toList();
return SizedBox(
width: double.infinity,
@@ -64,7 +65,7 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
builder: (context, constraints) {
if (state.files.isEmpty) {
return _AddFileButton(
controller: addFileController,
controller: popoverController,
direction: PopoverDirection.bottomWithLeftAligned,
mutex: mutex,
child: FlowyHover(
@@ -73,7 +74,7 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
AFThemeExtension.of(context).lightGreyHover,
),
child: GestureDetector(
onTap: addFileController.show,
onTap: popoverController.show,
behavior: HitTestBehavior.translucent,
child: Padding(
padding: const EdgeInsets.symmetric(
@@ -96,24 +97,27 @@ class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {
runSpacing: 12,
spacing: 12,
children: [
...filesToDisplay.map(
(file) => _FilePreviewRender(
...filesToDisplay.mapIndexed(
(index, file) => _FilePreviewRender(
key: ValueKey(file.id),
file: file,
file: state.files[index],
index: index,
images: images,
size: size,
mutex: mutex,
hideFileNames: state.hideFileNames,
),
),
SizedBox(
width: size,
height: size / 2,
child: _AddFileButton(
controller: addFileController,
controller: popoverController,
mutex: mutex,
child: FlowyHover(
resetHoverOnRebuild: false,
child: GestureDetector(
onTap: addFileController.show,
onTap: popoverController.show,
behavior: HitTestBehavior.translucent,
child: Padding(
padding: const EdgeInsets.all(8.0),
@@ -170,13 +174,15 @@ class _AddFileButton extends StatelessWidget {
mutex: mutex,
offset: const Offset(0, 10),
direction: direction,
constraints: const BoxConstraints(maxWidth: 350),
popupBuilder: (_) => FileUploadMenu(
onInsertLocalFile: (file) => insertLocalFile(
allowMultipleFiles: true,
onInsertLocalFile: (files) => insertLocalFiles(
context,
file,
files,
userProfile: context.read<MediaCellBloc>().state.userProfile,
documentId: context.read<MediaCellBloc>().rowId,
onUploadSuccess: (path, isLocalMode) {
onUploadSuccess: (file, path, isLocalMode) {
final mediaCellBloc = context.read<MediaCellBloc>();
if (mediaCellBloc.isClosed) {
return;
@@ -187,8 +193,8 @@ class _AddFileButton extends StatelessWidget {
url: path,
name: file.name,
uploadType: isLocalMode
? MediaUploadTypePB.LocalMedia
: MediaUploadTypePB.CloudMedia,
? FileUploadTypePB.LocalFile
: FileUploadTypePB.CloudFile,
fileType: file.fileType.toMediaFileTypePB(),
),
);
@@ -221,7 +227,7 @@ class _AddFileButton extends StatelessWidget {
MediaCellEvent.addFile(
url: url,
name: name,
uploadType: MediaUploadTypePB.NetworkMedia,
uploadType: FileUploadTypePB.NetworkFile,
fileType: fileType,
),
);
@@ -238,13 +244,19 @@ class _FilePreviewRender extends StatefulWidget {
const _FilePreviewRender({
super.key,
required this.file,
required this.images,
required this.index,
required this.size,
required this.mutex,
this.hideFileNames = false,
});
final MediaFilePB file;
final List<MediaFilePB> images;
final int index;
final double size;
final PopoverMutex mutex;
final bool hideFileNames;
@override
State<_FilePreviewRender> createState() => _FilePreviewRenderState();
@@ -257,41 +269,45 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
bool isHovering = false;
bool isSelected = false;
late int thisIndex;
MediaFilePB get file => widget.file;
@override
void initState() {
super.initState();
thisIndex = widget.images.indexOf(file);
}
@override
void dispose() {
controller.close();
super.dispose();
}
@override
void didUpdateWidget(covariant _FilePreviewRender oldWidget) {
thisIndex = widget.images.indexOf(file);
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
Widget child;
if (widget.file.fileType == MediaFileTypePB.Image) {
if (widget.file.uploadType == MediaUploadTypePB.NetworkMedia) {
child = Image.network(
widget.file.url,
fit: BoxFit.cover,
);
} else if (widget.file.uploadType == MediaUploadTypePB.LocalMedia) {
child = Image.file(
File(widget.file.url),
fit: BoxFit.cover,
);
} else {
// Cloud
child = FlowyNetworkImage(
url: widget.file.url,
userProfilePB: context.read<MediaCellBloc>().state.userProfile,
);
}
if (file.fileType == MediaFileTypePB.Image) {
child = AFImage(
url: file.url,
uploadType: file.uploadType,
userProfile: context.read<MediaCellBloc>().state.userProfile,
);
} else {
child = DecoratedBox(
decoration: BoxDecoration(color: widget.file.fileType.color),
decoration: BoxDecoration(color: file.fileType.color),
child: Center(
child: Container(
padding: const EdgeInsets.all(8),
child: FlowySvg(
widget.file.fileType.icon,
file.fileType.icon,
color: AFThemeExtension.of(context).strongText,
size: const Size.square(32),
),
@@ -302,14 +318,14 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
return AppFlowyPopover(
controller: controller,
constraints: const BoxConstraints(maxWidth: 150),
constraints: const BoxConstraints(maxWidth: 165),
offset: const Offset(0, 5),
onClose: () => setState(() => isSelected = false),
popupBuilder: (_) => SeparatedColumn(
separatorBuilder: () => const VSpace(4),
mainAxisSize: MainAxisSize.min,
children: [
if (widget.file.fileType == MediaFileTypePB.Image) ...[
if (file.fileType == MediaFileTypePB.Image) ...[
FlowyButton(
onTap: () {
controller.close();
@@ -319,15 +335,19 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
userProfile:
context.read<MediaCellBloc>().state.userProfile,
imageProvider: AFBlockImageProvider(
images: [
ImageBlockData(
url: widget.file.url,
type: widget.file.uploadType.toCustomImageType(),
),
],
onDeleteImage: (_) => context
.read<MediaCellBloc>()
.deleteFile(widget.file.id),
initialIndex: thisIndex,
images: widget.images
.map(
(e) => ImageBlockData(
url: e.url,
type: e.uploadType.toCustomImageType(),
),
)
.toList(),
onDeleteImage: (index) {
final deleteFile = widget.images[index];
context.read<MediaCellBloc>().deleteFile(deleteFile.id);
},
),
),
);
@@ -344,6 +364,31 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
leftIconSize: const Size(18, 18),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
),
FlowyButton(
onTap: () {
controller.close();
context.read<RowDetailBloc>().add(
RowDetailEvent.setCover(
RowCoverPB(
data: file.url,
uploadType: file.uploadType,
coverType: CoverTypePB.FileCover,
),
),
);
},
leftIcon: FlowySvg(
FlowySvgs.add_cover_s,
color: Theme.of(context).iconTheme.color,
size: const Size.square(18),
),
text: FlowyText.regular(
LocaleKeys.grid_media_setAsCover.tr(),
color: AFThemeExtension.of(context).textColor,
),
leftIconSize: const Size(18, 18),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
),
],
FlowyButton(
leftIcon: FlowySvg(
@@ -357,7 +402,7 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
onTap: () {
controller.close();
nameController.text = widget.file.name;
nameController.text = file.name;
nameController.selection = TextSelection(
baseOffset: 0,
extentOffset: nameController.text.length,
@@ -384,7 +429,7 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
FlowyButton(
onTap: () async => downloadMediaFile(
context,
widget.file,
file,
userProfile: context.read<MediaCellBloc>().state.userProfile,
),
leftIcon: FlowySvg(
@@ -404,11 +449,11 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
controller.close();
showConfirmDeletionDialog(
context: context,
name: widget.file.name,
name: file.name,
description: LocaleKeys.grid_media_deleteFileDescription.tr(),
onConfirm: () => context
.read<MediaCellBloc>()
.add(MediaCellEvent.removeFile(fileId: widget.file.id)),
.add(MediaCellEvent.removeFile(fileId: file.id)),
);
},
leftIcon: FlowySvg(
@@ -427,14 +472,17 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.file.fileType != MediaFileTypePB.Image
onTap: file.fileType != MediaFileTypePB.Image
? null
: () => openInteractiveViewerFromFile(
: () => openInteractiveViewerFromFiles(
context,
widget.file,
widget.images,
userProfile: context.read<MediaCellBloc>().state.userProfile,
onDeleteImage: (_) =>
context.read<MediaCellBloc>().deleteFile(widget.file.id),
initialIndex: thisIndex,
onDeleteImage: (index) {
final deleteFile = widget.images[index];
context.read<MediaCellBloc>().deleteFile(deleteFile.id);
},
),
child: FlowyHover(
isSelected: () => isSelected,
@@ -464,39 +512,46 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: AFThemeExtension.of(context).greyHover,
borderRadius: const BorderRadius.only(
borderRadius: BorderRadius.only(
topLeft: Corners.s6Radius,
topRight: Corners.s6Radius,
bottomLeft: widget.hideFileNames
? Corners.s6Radius
: Radius.zero,
bottomRight: widget.hideFileNames
? Corners.s6Radius
: Radius.zero,
),
),
child: child,
),
Container(
height: 28,
width: widget.size,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).isLightMode
? Theme.of(context).cardColor
: AFThemeExtension.of(context).greyHover,
borderRadius: const BorderRadius.only(
bottomLeft: Corners.s6Radius,
bottomRight: Corners.s6Radius,
if (!widget.hideFileNames)
Container(
height: 28,
width: widget.size,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).isLightMode
? Theme.of(context).cardColor
: AFThemeExtension.of(context).greyHover,
borderRadius: const BorderRadius.only(
bottomLeft: Corners.s6Radius,
bottomRight: Corners.s6Radius,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Center(
child: FlowyText.medium(
widget.file.name,
overflow: TextOverflow.ellipsis,
fontSize: 12,
color:
AFThemeExtension.of(context).secondaryTextColor,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Center(
child: FlowyText.medium(
file.name,
overflow: TextOverflow.ellipsis,
fontSize: 12,
color: AFThemeExtension.of(context)
.secondaryTextColor,
),
),
),
),
),
],
),
),
@@ -541,7 +596,7 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
context
.read<MediaCellBloc>()
.add(MediaCellEvent.renameFile(fileId: widget.file.id, name: newName));
.add(MediaCellEvent.renameFile(fileId: file.id, name: newName));
Navigator.of(context).pop();
}
}
@@ -79,10 +79,13 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listenWhen: (previous, current) => previous.content != current.content,
listener: (context, state) {
if (!focusNode.hasFocus) {
_textEditingController.text = state.content;
}
// It's essential to set the new content to the textEditingController.
// If you don't, the old value in textEditingController will persist and
// overwrite the correct value, leading to inconsistencies between the
// displayed text and the actual data.
_textEditingController.text = state.content ?? "";
},
child: Builder(
builder: (context) {
@@ -1,11 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
extension SelectOptionColorExtension on SelectOptionColorPB {
Color toColor(BuildContext context) {
@@ -101,7 +101,7 @@ class SelectOptionTag extends StatelessWidget {
color: optionColor,
borderRadius: BorderRadius.all(
Radius.circular(
PlatformExtension.isDesktopOrWeb ? 6 : 11,
UniversalPlatform.isDesktopOrWeb ? 6 : 11,
),
),
),
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
@@ -11,11 +9,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy/util/xfile_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:cross_file/cross_file.dart';
@@ -47,6 +46,10 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
Widget build(BuildContext context) {
return BlocBuilder<MediaCellBloc, MediaCellState>(
builder: (context, state) {
final images = state.files
.where((file) => file.fileType == MediaFileTypePB.Image)
.toList();
return Padding(
padding: const EdgeInsets.all(4),
child: SingleChildScrollView(
@@ -63,6 +66,7 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
value: context.read<MediaCellBloc>(),
child: RenderMedia(
file: state.files[index],
images: images,
index: index,
enableReordering: state.files.length > 1,
mutex: itemMutex,
@@ -92,13 +96,14 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
),
triggerActions: PopoverTriggerFlags.none,
popupBuilder: (popoverContext) => FileUploadMenu(
onInsertLocalFile: (file) async => insertLocalFile(
allowMultipleFiles: true,
onInsertLocalFile: (files) async => insertLocalFiles(
context,
file,
files,
userProfile:
context.read<MediaCellBloc>().state.userProfile,
documentId: context.read<MediaCellBloc>().rowId,
onUploadSuccess: (path, isLocalMode) {
onUploadSuccess: (file, path, isLocalMode) {
final mediaCellBloc = context.read<MediaCellBloc>();
if (mediaCellBloc.isClosed) {
return;
@@ -109,8 +114,8 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
url: path,
name: file.name,
uploadType: isLocalMode
? MediaUploadTypePB.LocalMedia
: MediaUploadTypePB.CloudMedia,
? FileUploadTypePB.LocalFile
: FileUploadTypePB.CloudFile,
fileType: file.fileType.toMediaFileTypePB(),
),
);
@@ -146,7 +151,7 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
MediaCellEvent.addFile(
url: url,
name: name,
uploadType: MediaUploadTypePB.NetworkMedia,
uploadType: FileUploadTypePB.NetworkFile,
fileType: fileType,
),
);
@@ -187,10 +192,10 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
}
}
extension ToCustomImageType on MediaUploadTypePB {
extension ToCustomImageType on FileUploadTypePB {
CustomImageType toCustomImageType() => switch (this) {
MediaUploadTypePB.NetworkMedia => CustomImageType.external,
MediaUploadTypePB.CloudMedia => CustomImageType.internal,
FileUploadTypePB.NetworkFile => CustomImageType.external,
FileUploadTypePB.CloudFile => CustomImageType.internal,
_ => CustomImageType.local,
};
}
@@ -201,12 +206,14 @@ class RenderMedia extends StatefulWidget {
super.key,
required this.index,
required this.file,
required this.images,
required this.enableReordering,
required this.mutex,
});
final int index;
final MediaFilePB file;
final List<MediaFilePB> images;
final bool enableReordering;
final PopoverMutex mutex;
@@ -216,6 +223,21 @@ class RenderMedia extends StatefulWidget {
class _RenderMediaState extends State<RenderMedia> {
bool isHovering = false;
int? imageIndex;
MediaFilePB get file => widget.file;
@override
void initState() {
super.initState();
imageIndex = widget.images.indexOf(file);
}
@override
void didUpdateWidget(covariant RenderMedia oldWidget) {
imageIndex = widget.images.indexOf(file);
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
@@ -242,53 +264,35 @@ class _RenderMediaState extends State<RenderMedia> {
child: const FlowySvg(FlowySvgs.drag_element_s),
),
const HSpace(8),
if (widget.file.fileType == MediaFileTypePB.Image &&
widget.file.uploadType == MediaUploadTypePB.CloudMedia) ...[
if (widget.file.fileType == MediaFileTypePB.Image) ...[
Expanded(
child: _openInteractiveViewer(
context,
file: widget.file,
child: FlowyNetworkImage(
files: widget.images,
index: imageIndex!,
child: AFImage(
url: widget.file.url,
userProfilePB:
uploadType: widget.file.uploadType,
userProfile:
context.read<MediaCellBloc>().state.userProfile,
),
),
),
] else if (widget.file.fileType == MediaFileTypePB.Image) ...[
Expanded(
child: _openInteractiveViewer(
context,
file: widget.file,
child: widget.file.uploadType ==
MediaUploadTypePB.NetworkMedia
? Image.network(
widget.file.url,
fit: BoxFit.cover,
alignment: Alignment.centerLeft,
)
: Image.file(
File(widget.file.url),
fit: BoxFit.cover,
alignment: Alignment.centerLeft,
),
),
),
] else ...[
Expanded(
child: GestureDetector(
onTap: () => afLaunchUrlString(widget.file.url),
onTap: () => afLaunchUrlString(file.url),
child: Row(
children: [
FlowySvg(
widget.file.fileType.icon,
file.fileType.icon,
color: AFThemeExtension.of(context).strongText,
size: const Size.square(18),
),
const HSpace(8),
Flexible(
child: FlowyText(
widget.file.name,
file.name,
overflow: TextOverflow.ellipsis,
),
),
@@ -307,7 +311,7 @@ class _RenderMediaState extends State<RenderMedia> {
popupBuilder: (popoverContext) => BlocProvider.value(
value: context.read<MediaCellBloc>(),
child: MediaItemMenu(
file: widget.file,
file: file,
closeContext: popoverContext,
),
),
@@ -330,17 +334,21 @@ class _RenderMediaState extends State<RenderMedia> {
Widget _openInteractiveViewer(
BuildContext context, {
required MediaFilePB file,
required List<MediaFilePB> files,
required int index,
required Widget child,
}) =>
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => openInteractiveViewerFromFile(
onTap: () => openInteractiveViewerFromFiles(
context,
file,
onDeleteImage: (_) =>
context.read<MediaCellBloc>().deleteFile(file.id),
files,
onDeleteImage: (index) {
final deleteFile = files[index];
context.read<MediaCellBloc>().deleteFile(deleteFile.id);
},
userProfile: context.read<MediaCellBloc>().state.userProfile,
initialIndex: index,
),
child: child,
);
Loaded 100 of 594 files, more files were not shown because too many files have changed in this diff. Show more