Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e125e9493a | ||
|
|
5fbd07e96a | ||
|
|
0a6bee6232 | ||
|
|
552f2a73de | ||
|
|
eeb6b69f58 | ||
|
|
99889b9950 | ||
|
|
c51b495544 | ||
|
|
0d2841227a | ||
|
|
2dc7dc1a26 | ||
|
|
f0a494b41d | ||
|
|
806fc75a8a | ||
|
|
d3362bf96a | ||
|
|
7bef698f03 | ||
|
|
bea4b60a7a | ||
|
|
1100c8b7e1 | ||
|
|
728c75ff87 | ||
|
|
e01a59c772 | ||
|
|
164933325c | ||
|
|
88df232139 | ||
|
|
7d3159c8da | ||
|
|
e36f9aee4e | ||
|
|
0a5f98014c | ||
|
|
e5cb16f5b3 | ||
|
|
aa06c78b68 | ||
|
|
da03f40d29 | ||
|
|
6ddbb418eb | ||
|
|
f9f5ae04d0 | ||
|
|
71e7f54367 | ||
|
|
c5afbb97a0 | ||
|
|
d189c1ca4e | ||
|
|
630fdb8995 | ||
|
|
522143cfd8 | ||
|
|
1dedb909e5 | ||
|
|
605a079927 | ||
|
|
938abb396e | ||
|
|
a406dfbc43 | ||
|
|
f303674737 | ||
|
|
2bc9753f60 | ||
|
|
56cc7bb972 | ||
|
|
710e894446 | ||
|
|
d67d77f442 | ||
|
|
129db6925c | ||
|
|
924df56d28 | ||
|
|
c2b194b1d2 | ||
|
|
68986bd170 | ||
|
|
e4029d05dc | ||
|
|
f1bc9f860c | ||
|
|
b865037728 | ||
|
|
7e0de1f69c | ||
|
|
12c575dbbe | ||
|
|
f44fb02e4a | ||
|
|
4df59ca421 | ||
|
|
7fee00ab72 | ||
|
|
7271e0a108 | ||
|
|
877236baf9 | ||
|
|
2c66634c89 | ||
|
|
609ea27c42 | ||
|
|
aafdb80455 | ||
|
|
4888d5d528 | ||
|
|
21cc130e4e | ||
|
|
a02de1a435 | ||
|
|
477683d454 | ||
|
|
340f37e00d | ||
|
|
eea3004a97 | ||
|
|
f59772756a | ||
|
|
1cf4f493db | ||
|
|
c5a9dea721 | ||
|
|
7d2719dd71 | ||
|
|
2ef896fb4a | ||
|
|
a0c6fb2fe8 | ||
|
|
ee3054b355 | ||
|
|
bfe6d25322 |
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
+60
@@ -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)));
|
||||
});
|
||||
});
|
||||
}
|
||||
+67
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+48
-51
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+96
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+36
-35
@@ -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();
|
||||
}
|
||||
+22
-22
@@ -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();
|
||||
});
|
||||
|
||||
+217
-1
@@ -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()]);
|
||||
});
|
||||
});
|
||||
}
|
||||
+191
@@ -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()]);
|
||||
});
|
||||
});
|
||||
}
|
||||
+24
-29
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -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');
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
+2
-1
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+120
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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();
|
||||
}
|
||||
+103
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+122
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+58
-1
@@ -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';
|
||||
@@ -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) {
|
||||
|
||||
+29
-24
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+207
-79
@@ -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,
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
|
||||
),
|
||||
onPressed: () {
|
||||
final name = state.content;
|
||||
_openRowPage(context, name);
|
||||
_openRowPage(context, name ?? "");
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+23
-47
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
+16
-2
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-6
@@ -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:
|
||||
|
||||
+7
@@ -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);
|
||||
}
|
||||
}
|
||||
+16
-4
@@ -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(
|
||||
|
||||
+40
-8
@@ -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,
|
||||
});
|
||||
|
||||
-113
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
+58
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+140
-195
@@ -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,
|
||||
|
||||
+258
@@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -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();
|
||||
},
|
||||
|
||||
+10
@@ -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();
|
||||
|
||||
+9
-9
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
+15
-9
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+3
-4
@@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+20
-16
@@ -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);
|
||||
},
|
||||
|
||||
+29
-1
@@ -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
|
||||
|
||||
+36
-4
@@ -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,
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+38
-17
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+61
-45
@@ -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});
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
+46
-4
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -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,
|
||||
|
||||
+19
-36
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-33
@@ -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();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+138
-83
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+53
-45
@@ -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
Reference in new issue
Block a user