mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-10-05 23:07:27 +03:00
Merge branch 'main' into main
This commit is contained in:
commit
9076296748
26
CHANGELOG.md
26
CHANGELOG.md
@ -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'
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import 'anon_user_continue_test.dart' as anon_user_continue_test;
|
||||
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
|
||||
import 'document/document_drag_block_test.dart' as document_drag_block_test;
|
||||
import 'empty_test.dart' as preset_af_cloud_env_test;
|
||||
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
|
||||
import 'user_setting_sync_test.dart' as user_sync_test;
|
||||
import 'workspace/change_name_and_icon_test.dart'
|
||||
as change_workspace_name_and_icon_test;
|
||||
import 'workspace/collaborative_workspace_test.dart'
|
||||
as collaboration_workspace_test;
|
||||
import 'workspace/workspace_settings_test.dart' as workspace_settings_test;
|
||||
|
||||
Future<void> main() async {
|
||||
preset_af_cloud_env_test.main();
|
||||
@ -16,4 +19,11 @@ Future<void> main() async {
|
||||
// workspace
|
||||
collaboration_workspace_test.main();
|
||||
change_workspace_name_and_icon_test.main();
|
||||
workspace_settings_test.main();
|
||||
|
||||
// document
|
||||
document_drag_block_test.main();
|
||||
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
// 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 '../../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('document drag block: ', () {
|
||||
testWidgets('drag block to the top', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open getting started page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
|
||||
// before move
|
||||
final beforeMoveBlock = tester.editor.getNodeAtPath([1]);
|
||||
|
||||
// move the desktop guide to the top, above the getting started
|
||||
await tester.editor.dragBlock(
|
||||
[1],
|
||||
const Offset(20, -80),
|
||||
);
|
||||
|
||||
// wait for the move animation to complete
|
||||
await tester.pumpAndSettle(Durations.short1);
|
||||
|
||||
// check if the block is moved to the top
|
||||
final afterMoveBlock = tester.editor.getNodeAtPath([0]);
|
||||
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
|
||||
});
|
||||
|
||||
testWidgets('drag block to other block\'s child', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open getting started page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
|
||||
// before move
|
||||
final beforeMoveBlock = tester.editor.getNodeAtPath([10]);
|
||||
|
||||
// move the checkbox to the child of the block at path [9]
|
||||
await tester.editor.dragBlock(
|
||||
[10],
|
||||
const Offset(80, -30),
|
||||
);
|
||||
|
||||
// wait for the move animation to complete
|
||||
await tester.pumpAndSettle(Durations.short1);
|
||||
|
||||
// check if the block is moved to the child of the block at path [9]
|
||||
final afterMoveBlock = tester.editor.getNodeAtPath([9, 0]);
|
||||
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../shared/constants.dart';
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/dir.dart';
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('sidebar move page: ', () {
|
||||
testWidgets('create a new document and move it to Getting started',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
// click the ... button and move to Getting started
|
||||
await tester.hoverOnPageName(
|
||||
pageName,
|
||||
onHover: () async {
|
||||
await tester.tapPageOptionButton();
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.disclosureAction_moveTo.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// expect to see two pages
|
||||
// one is in the sidebar, the other is in the move to page list
|
||||
// 1. Getting started
|
||||
// 2. To-dos
|
||||
final gettingStarted = find.findTextInFlowyText(
|
||||
Constants.gettingStartedPageName,
|
||||
);
|
||||
final toDos = find.findTextInFlowyText(Constants.toDosPageName);
|
||||
await tester.pumpUntilFound(gettingStarted);
|
||||
await tester.pumpUntilFound(toDos);
|
||||
expect(gettingStarted, findsNWidgets(2));
|
||||
|
||||
// skip the length check on Linux temporarily,
|
||||
// because it failed in expect check but the previous pumpUntilFound is successful
|
||||
if (!UniversalPlatform.isLinux) {
|
||||
expect(toDos, findsNWidgets(2));
|
||||
|
||||
// hover on the todos page, and will see a forbidden icon
|
||||
await tester.hoverOnWidget(
|
||||
toDos.last,
|
||||
onHover: () async {
|
||||
final tooltips = find.byTooltip(
|
||||
LocaleKeys.space_cannotMovePageToDatabase.tr(),
|
||||
);
|
||||
expect(tooltips, findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// move the current page to Getting started
|
||||
await tester.tapButton(
|
||||
gettingStarted.last,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// after moving, expect to not see the page name in the sidebar
|
||||
final page = tester.findPageName(pageName);
|
||||
expect(page, findsNothing);
|
||||
|
||||
// click to expand the getting started page
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: Constants.gettingStartedPageName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect to see the page name in the getting started page
|
||||
final pageInGettingStarted = tester.findPageName(
|
||||
pageName,
|
||||
parentName: Constants.gettingStartedPageName,
|
||||
);
|
||||
expect(pageInGettingStarted, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
@ -32,69 +32,66 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// final email = '${uuid()}@appflowy.io';
|
||||
|
||||
group('collaborative workspace', () {
|
||||
group('collaborative workspace: ', () {
|
||||
// combine the create and delete workspace test to reduce the time
|
||||
testWidgets('create a new workspace, open it and then delete it',
|
||||
(tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
// if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
// return;
|
||||
// }
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// await tester.initializeAppFlowy(
|
||||
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
// email: email,
|
||||
// );
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// const name = 'AppFlowy.IO';
|
||||
// // the workspace will be opened after created
|
||||
// await tester.createCollaborativeWorkspace(name);
|
||||
const name = 'AppFlowy.IO';
|
||||
// the workspace will be opened after created
|
||||
await tester.createCollaborativeWorkspace(name);
|
||||
|
||||
// final loading = find.byType(Loading);
|
||||
// await tester.pumpUntilNotFound(loading);
|
||||
final loading = find.byType(Loading);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
|
||||
// Finder success;
|
||||
Finder success;
|
||||
|
||||
// final Finder items = find.byType(WorkspaceMenuItem);
|
||||
final Finder items = find.byType(WorkspaceMenuItem);
|
||||
|
||||
// // delete the newly created workspace
|
||||
// await tester.openCollaborativeWorkspaceMenu();
|
||||
// await tester.pumpUntilFound(items);
|
||||
// delete the newly created workspace
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.pumpUntilFound(items);
|
||||
|
||||
// expect(items, findsNWidgets(2));
|
||||
// expect(
|
||||
// tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||
// name,
|
||||
// );
|
||||
expect(items, findsNWidgets(2));
|
||||
expect(
|
||||
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||
name,
|
||||
);
|
||||
|
||||
// final secondWorkspace = find.byType(WorkspaceMenuItem).last;
|
||||
// await tester.hoverOnWidget(
|
||||
// secondWorkspace,
|
||||
// onHover: () async {
|
||||
// // click the more button
|
||||
// final moreButton = find.byType(WorkspaceMoreActionList);
|
||||
// expect(moreButton, findsOneWidget);
|
||||
// await tester.tapButton(moreButton);
|
||||
// // click the delete button
|
||||
// final deleteButton = find.text(LocaleKeys.button_delete.tr());
|
||||
// expect(deleteButton, findsOneWidget);
|
||||
// await tester.tapButton(deleteButton);
|
||||
// // see the delete confirm dialog
|
||||
// final confirm =
|
||||
// find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
|
||||
// expect(confirm, findsOneWidget);
|
||||
// await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
// // delete success
|
||||
// success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
// await tester.pumpUntilFound(success);
|
||||
// expect(success, findsOneWidget);
|
||||
// await tester.pumpUntilNotFound(success);
|
||||
// },
|
||||
// );
|
||||
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
|
||||
await tester.hoverOnWidget(
|
||||
secondWorkspace,
|
||||
onHover: () async {
|
||||
// click the more button
|
||||
final moreButton = find.byType(WorkspaceMoreActionList);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
// click the delete button
|
||||
final deleteButton = find.text(LocaleKeys.button_delete.tr());
|
||||
expect(deleteButton, findsOneWidget);
|
||||
await tester.tapButton(deleteButton);
|
||||
// see the delete confirm dialog
|
||||
final confirm =
|
||||
find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
|
||||
expect(confirm, findsOneWidget);
|
||||
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
// delete success
|
||||
success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
await tester.pumpUntilFound(success);
|
||||
expect(success, findsOneWidget);
|
||||
await tester.pumpUntilNotFound(success);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,96 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/dir.dart';
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('workspace settings: ', () {
|
||||
testWidgets(
|
||||
'change document width',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.workspace);
|
||||
|
||||
final documentWidthSettings = find.findTextInFlowyText(
|
||||
LocaleKeys.settings_appearance_documentSettings_width.tr(),
|
||||
);
|
||||
|
||||
final scrollable = find.ancestor(
|
||||
of: find.byType(SettingsWorkspaceView),
|
||||
matching: find.descendant(
|
||||
of: find.byType(SingleChildScrollView),
|
||||
matching: find.byType(Scrollable),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.scrollUntilVisible(
|
||||
documentWidthSettings,
|
||||
0,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// change the document width
|
||||
final slider = find.byType(Slider);
|
||||
final oldValue = tester.widget<Slider>(slider).value;
|
||||
await tester.drag(slider, const Offset(-100, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// check the document width is changed
|
||||
expect(tester.widget<Slider>(slider).value, lessThan(oldValue));
|
||||
|
||||
// click the reset button
|
||||
final resetButton = find.descendant(
|
||||
of: find.byType(DocumentPaddingSetting),
|
||||
matching: find.byType(SettingsResetButton),
|
||||
);
|
||||
await tester.tap(resetButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// check the document width is reset
|
||||
expect(
|
||||
tester.widget<Slider>(slider).value,
|
||||
EditorStyleCustomizer.maxDocumentWidth,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -25,6 +25,7 @@ void main() {
|
||||
name: fieldName,
|
||||
layout: ViewLayoutPB.Board,
|
||||
);
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.tapButton(card1);
|
||||
await tester.changeFieldTypeOfFieldWithName(
|
||||
fieldName,
|
||||
|
@ -1,13 +1,14 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@ -77,47 +78,47 @@ void main() {
|
||||
shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length;
|
||||
expect(shownGroups, 4);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('delete a group', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
|
||||
testWidgets('delete a group', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
|
||||
|
||||
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
|
||||
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
|
||||
|
||||
// tap group option button for the first group. Delete shouldn't show up
|
||||
await tester.tapButton(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(BoardColumnHeader),
|
||||
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
|
||||
// tap group option button for the first group. Delete shouldn't show up
|
||||
await tester.tapButton(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(BoardColumnHeader),
|
||||
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
|
||||
|
||||
// dismiss the popup
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
// dismiss the popup
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// tap group option button for the first group. Delete should show up
|
||||
await tester.tapButton(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(BoardColumnHeader),
|
||||
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
|
||||
)
|
||||
.at(1),
|
||||
);
|
||||
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
|
||||
// tap group option button for the first group. Delete should show up
|
||||
await tester.tapButton(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(BoardColumnHeader),
|
||||
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
|
||||
)
|
||||
.at(1),
|
||||
);
|
||||
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
|
||||
|
||||
// Tap the delete button and confirm
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
|
||||
await tester.tapDialogOkButton();
|
||||
// Tap the delete button and confirm
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
|
||||
await tester.tapButtonWithName(LocaleKeys.space_delete.tr());
|
||||
|
||||
// Expect number of groups to decrease by one
|
||||
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
|
||||
// Expect number of groups to decrease by one
|
||||
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
|
||||
await tester.tapOKButton();
|
||||
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
|
||||
expect(find.text(name), findsNothing);
|
||||
});
|
||||
|
||||
@ -51,6 +51,37 @@ void main() {
|
||||
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('duplicate item in ToDo card then delete', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
|
||||
const name = 'Card 1';
|
||||
final card1 = find.text(name);
|
||||
await tester.hoverOnWidget(
|
||||
card1,
|
||||
onHover: () async {
|
||||
final moreOption = find.byType(MoreCardOptionsAccessory);
|
||||
await tester.tapButton(moreOption);
|
||||
},
|
||||
);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
|
||||
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
|
||||
|
||||
// get the last widget that contains the name
|
||||
final duplicatedCard = find.textContaining(name, findRichText: true).last;
|
||||
await tester.hoverOnWidget(
|
||||
duplicatedCard,
|
||||
onHover: () async {
|
||||
final moreOption = find.byType(MoreCardOptionsAccessory);
|
||||
await tester.tapButton(moreOption);
|
||||
},
|
||||
);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
|
||||
expect(find.textContaining(name, findRichText: true), findsNWidgets(1));
|
||||
});
|
||||
|
||||
testWidgets('add new group', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'board_add_row_test.dart' as board_add_row_test;
|
||||
import 'board_group_test.dart' as board_group_test;
|
||||
import 'board_row_test.dart' as board_row_test;
|
||||
import 'board_field_test.dart' as board_field_test;
|
||||
import 'board_hide_groups_test.dart' as board_hide_groups_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@ -11,4 +13,6 @@ void main() {
|
||||
board_row_test.main();
|
||||
board_add_row_test.main();
|
||||
board_group_test.main();
|
||||
board_field_test.main();
|
||||
board_hide_groups_test.main();
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ void main() {
|
||||
|
||||
group('grid filter:', () {
|
||||
testWidgets('add text filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
@ -19,68 +19,68 @@ void main() {
|
||||
await tester.tapFilterButtonInGrid('Name');
|
||||
|
||||
// enter 'A' in the filter text field
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
tester.assertNumberOfRowsInGridPage(10);
|
||||
await tester.enterTextInTextFilter('A');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// after remove the filter, the grid should show all rows
|
||||
await tester.enterTextInTextFilter('');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.enterTextInTextFilter('B');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// open the menu to delete the filter
|
||||
await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checkbox filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Done');
|
||||
await tester.tapCheckboxFilterButtonInGrid();
|
||||
|
||||
await tester.tapUnCheckedButtonOnCheckboxFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester
|
||||
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checklist filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');
|
||||
|
||||
// By default, the condition of checklist filter is 'uncompleted'
|
||||
await tester.assertNumberOfRowsInGridPage(9);
|
||||
tester.assertNumberOfRowsInGridPage(9);
|
||||
|
||||
await tester.tapFilterButtonInGrid('checklist');
|
||||
await tester.tapChecklistFilterButtonInGrid();
|
||||
|
||||
await tester.tapCompletedButtonOnChecklistFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add single select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
@ -90,27 +90,27 @@ void main() {
|
||||
|
||||
// select the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(0);
|
||||
tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
// unselect the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
// select the option 's5'
|
||||
await tester.tapOptionFilterWithName('s5');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// select the option 's4'
|
||||
await tester.tapOptionFilterWithName('s4');
|
||||
|
||||
// The row with 's4' should be shown.
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add multi select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
@ -124,17 +124,17 @@ void main() {
|
||||
|
||||
// select the option 'm1'. Any option with 'm1' should be shown.
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
tester.assertNumberOfRowsInGridPage(5);
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
|
||||
// select the option 'm2'. Any option with 'm2' should be shown.
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
|
||||
// select the option 'm4'. Any option with 'm4' should be shown.
|
||||
await tester.tapOptionFilterWithName('m4');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
@ -0,0 +1,188 @@
|
||||
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/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(BannerCover), findsOneWidget);
|
||||
|
||||
// Remove the temp file
|
||||
await Future.wait([file.delete()]);
|
||||
});
|
||||
|
||||
testWidgets('upload 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 (BannerCover is always in the Widget tree - thus check AFImage)
|
||||
expect(find.byType(AFImage), findsNothing);
|
||||
|
||||
// Hover on RowBanner to show Add Cover button
|
||||
await tester.hoverRowBanner();
|
||||
|
||||
// Click on Add Cover button
|
||||
await tester.tapAddCoverButton();
|
||||
|
||||
// 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');
|
||||
|
||||
// Tap on the upload image button
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect a cover to be shown
|
||||
expect(find.byType(AFImage), findsOneWidget);
|
||||
|
||||
// Tap on the delete cover button
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_cover_removeCover.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect no cover to be shown
|
||||
expect(find.byType(AFImage), findsNothing);
|
||||
|
||||
// Remove the temp file
|
||||
await Future.wait([file.delete()]);
|
||||
});
|
||||
|
||||
testWidgets('upload cover and check in Board', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
|
||||
|
||||
// Open "Card 1"
|
||||
await tester.tap(find.text('Card 1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect no cover (BannerCover is always in the Widget tree - thus check AFImage)
|
||||
expect(find.byType(AFImage), findsNothing);
|
||||
|
||||
// Hover on RowBanner to show Add Cover button
|
||||
await tester.hoverRowBanner();
|
||||
|
||||
// Click on Add Cover button
|
||||
await tester.tapAddCoverButton();
|
||||
|
||||
// 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');
|
||||
|
||||
// Tap on the upload image button
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 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()]);
|
||||
});
|
||||
});
|
||||
}
|
@ -321,7 +321,7 @@ void main() {
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
tester.assertNumberOfRowsInGridPage(2);
|
||||
});
|
||||
|
||||
testWidgets('duplicate row', (tester) async {
|
||||
@ -338,7 +338,7 @@ void main() {
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
tester.assertNumberOfRowsInGridPage(4);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
testWidgets('create row of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateRowButtonInGrid();
|
||||
|
||||
// 3 initial rows + 1 created
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create row from row menu of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
|
||||
await tester.tapCreateRowButtonInRowMenuOfGrid();
|
||||
|
||||
// 3 initial rows + 1 created
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('delete row of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.hoverOnFirstRowOfGrid(() async {
|
||||
// Open the row menu and then click the delete
|
||||
await tester.tapRowMenuButtonInGrid();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapDeleteOnRowMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 3 initial rows - 1 deleted
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('check number of row indicator in the initial grid',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
@ -9,7 +9,7 @@ void main() {
|
||||
|
||||
group('database', () {
|
||||
testWidgets('import v0.2.0 database data', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
// wait the database data is loaded
|
||||
await tester.pumpAndSettle(const Duration(microseconds: 500));
|
||||
|
||||
|
@ -9,7 +9,7 @@ void main() {
|
||||
|
||||
group('grid', () {
|
||||
testWidgets('add text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
@ -60,7 +60,7 @@ void main() {
|
||||
|
||||
// delete all sorts
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapAllSortButton();
|
||||
await tester.tapDeleteAllSortsButton();
|
||||
|
||||
// check the text cell order
|
||||
for (final (index, content) in <String>[
|
||||
@ -85,7 +85,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('add checkbox sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
@ -135,7 +135,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('add number sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
@ -187,7 +187,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('add checkbox and number sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
@ -266,7 +266,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('reorder sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
await tester.openTestDatabase(v020GridFileName);
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
|
@ -0,0 +1,203 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
import 'grid_test_extensions.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid create row test:', () {
|
||||
testWidgets('from the bottom', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
final expected = tester.getGridRows();
|
||||
|
||||
// create row
|
||||
await tester.tapCreateRowButtonInGrid();
|
||||
|
||||
final actual = tester.getGridRows();
|
||||
expect(actual.slice(0, 3), orderedEquals(expected));
|
||||
expect(actual.length, equals(4));
|
||||
tester.assertNumberOfRowsInGridPage(4);
|
||||
});
|
||||
|
||||
testWidgets('from a row\'s menu', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
final expected = tester.getGridRows();
|
||||
|
||||
// create row
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
|
||||
|
||||
final actual = tester.getGridRows();
|
||||
expect([actual[0], actual[2], actual[3]], orderedEquals(expected));
|
||||
expect(actual.length, equals(4));
|
||||
tester.assertNumberOfRowsInGridPage(4);
|
||||
});
|
||||
|
||||
testWidgets('with sort configured', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final unsorted = tester.getGridRows();
|
||||
|
||||
// add a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
final sorted = [
|
||||
unsorted[7],
|
||||
unsorted[8],
|
||||
unsorted[1],
|
||||
unsorted[9],
|
||||
unsorted[11],
|
||||
unsorted[10],
|
||||
unsorted[6],
|
||||
unsorted[12],
|
||||
unsorted[2],
|
||||
unsorted[0],
|
||||
unsorted[3],
|
||||
unsorted[5],
|
||||
unsorted[4],
|
||||
];
|
||||
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(sorted));
|
||||
|
||||
// create row
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
|
||||
|
||||
// cancel
|
||||
expect(find.byType(ConfirmPopup), findsOneWidget);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_cancel.tr());
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(sorted));
|
||||
|
||||
// try again, but confirm this time
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
|
||||
expect(find.byType(ConfirmPopup), findsOneWidget);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_remove.tr());
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual.length, equals(14));
|
||||
tester.assertNumberOfRowsInGridPage(14);
|
||||
});
|
||||
|
||||
testWidgets('with filter configured', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final original = tester.getGridRows();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.Checkbox,
|
||||
'Registration Complete',
|
||||
);
|
||||
|
||||
final filtered = [
|
||||
original[1],
|
||||
original[3],
|
||||
original[5],
|
||||
original[6],
|
||||
original[7],
|
||||
original[9],
|
||||
original[12],
|
||||
];
|
||||
|
||||
// verify grid data
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filtered));
|
||||
|
||||
// create row (one before and after the first row, and one at the bottom)
|
||||
await tester.tapCreateRowButtonInGrid();
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
await tester.tapCreateRowButtonAfterHoveringOnGridRow();
|
||||
await tester.hoverOnFirstRowOfGrid(() async {
|
||||
await tester.tapRowMenuButtonInGrid();
|
||||
await tester.tapCreateRowAboveButtonInRowMenu();
|
||||
});
|
||||
|
||||
actual = tester.getGridRows();
|
||||
expect(actual.length, equals(10));
|
||||
tester.assertNumberOfRowsInGridPage(10);
|
||||
actual = [
|
||||
actual[1],
|
||||
actual[3],
|
||||
actual[4],
|
||||
actual[5],
|
||||
actual[6],
|
||||
actual[7],
|
||||
actual[8],
|
||||
];
|
||||
expect(actual, orderedEquals(filtered));
|
||||
|
||||
// delete the filter
|
||||
await tester.tapFilterButtonInGrid('Registration Complete');
|
||||
await tester
|
||||
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual.length, equals(16));
|
||||
tester.assertNumberOfRowsInGridPage(16);
|
||||
actual = [
|
||||
actual[0],
|
||||
actual[2],
|
||||
actual[4],
|
||||
actual[5],
|
||||
actual[6],
|
||||
actual[7],
|
||||
actual[8],
|
||||
actual[9],
|
||||
actual[10],
|
||||
actual[11],
|
||||
actual[12],
|
||||
actual[13],
|
||||
actual[14],
|
||||
];
|
||||
expect(actual, orderedEquals(original));
|
||||
});
|
||||
|
||||
// TODO(RS): move to somewhere else
|
||||
testWidgets('delete row of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.hoverOnFirstRowOfGrid(() async {
|
||||
// Open the row menu and then click the delete
|
||||
await tester.tapRowMenuButtonInGrid();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapDeleteOnRowMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 3 initial rows - 1 deleted
|
||||
tester.assertNumberOfRowsInGridPage(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import 'grid_test_extensions.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid simultaneous sort and filter test:', () {
|
||||
// testWidgets('delete filter with active sort', (tester) async {
|
||||
// await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// // get grid data
|
||||
// final original = tester.getGridRows();
|
||||
|
||||
// // add a filter
|
||||
// await tester.tapDatabaseFilterButton();
|
||||
// await tester.tapCreateFilterByFieldType(
|
||||
// FieldType.Checkbox,
|
||||
// 'Registration Complete',
|
||||
// );
|
||||
|
||||
// // add a sort
|
||||
// await tester.tapDatabaseSortButton();
|
||||
// await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
// final filteredAndSorted = [
|
||||
// original[7],
|
||||
// original[1],
|
||||
// original[9],
|
||||
// original[6],
|
||||
// original[12],
|
||||
// original[3],
|
||||
// original[5],
|
||||
// ];
|
||||
|
||||
// // verify grid data
|
||||
// List actual = tester.getGridRows();
|
||||
// expect(actual, orderedEquals(filteredAndSorted));
|
||||
|
||||
// // delete the filter
|
||||
// await tester.tapFilterButtonInGrid('Registration Complete');
|
||||
// await tester
|
||||
// .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
// await tester.tapDeleteFilterButtonInGrid();
|
||||
|
||||
// final sorted = [
|
||||
// original[7],
|
||||
// original[8],
|
||||
// original[1],
|
||||
// original[9],
|
||||
// original[11],
|
||||
// original[10],
|
||||
// original[6],
|
||||
// original[12],
|
||||
// original[2],
|
||||
// original[0],
|
||||
// original[3],
|
||||
// original[5],
|
||||
// original[4],
|
||||
// ];
|
||||
|
||||
// // verify grid data
|
||||
// actual = tester.getGridRows();
|
||||
// expect(actual, orderedEquals(sorted));
|
||||
// });
|
||||
|
||||
testWidgets('delete sort with active fiilter', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final original = tester.getGridRows();
|
||||
|
||||
// add a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.Checkbox,
|
||||
'Registration Complete',
|
||||
);
|
||||
|
||||
// add a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
final filteredAndSorted = [
|
||||
original[7],
|
||||
original[1],
|
||||
original[9],
|
||||
original[6],
|
||||
original[12],
|
||||
original[3],
|
||||
original[5],
|
||||
];
|
||||
|
||||
// verify grid data
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filteredAndSorted));
|
||||
|
||||
// delete the sort
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapDeleteAllSortsButton();
|
||||
|
||||
final filtered = [
|
||||
original[1],
|
||||
original[3],
|
||||
original[5],
|
||||
original[6],
|
||||
original[7],
|
||||
original[9],
|
||||
original[12],
|
||||
];
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filtered));
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
import 'grid_test_extensions.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid reopen test:', () {
|
||||
testWidgets('base case', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
final expected = tester.getGridRows();
|
||||
|
||||
// go to another page and come back
|
||||
await tester.openPage('Getting started');
|
||||
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
|
||||
|
||||
// verify grid data
|
||||
final actual = tester.getGridRows();
|
||||
|
||||
expect(actual, orderedEquals(expected));
|
||||
});
|
||||
|
||||
testWidgets('with sort configured', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final unsorted = tester.getGridRows();
|
||||
|
||||
// add a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
final sorted = [
|
||||
unsorted[7],
|
||||
unsorted[8],
|
||||
unsorted[1],
|
||||
unsorted[9],
|
||||
unsorted[11],
|
||||
unsorted[10],
|
||||
unsorted[6],
|
||||
unsorted[12],
|
||||
unsorted[2],
|
||||
unsorted[0],
|
||||
unsorted[3],
|
||||
unsorted[5],
|
||||
unsorted[4],
|
||||
];
|
||||
|
||||
// verify grid data
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(sorted));
|
||||
|
||||
// go to another page and come back
|
||||
await tester.openPage('Getting started');
|
||||
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(sorted));
|
||||
|
||||
// delete sorts
|
||||
// TODO(RS): Shouldn't the sort/filter list show automatically!?
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapDeleteAllSortsButton();
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(unsorted));
|
||||
|
||||
// go to another page and come back
|
||||
await tester.openPage('Getting started');
|
||||
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(unsorted));
|
||||
});
|
||||
|
||||
testWidgets('with filter configured', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final unfiltered = tester.getGridRows();
|
||||
|
||||
// add a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.Checkbox,
|
||||
'Registration Complete',
|
||||
);
|
||||
|
||||
final filtered = [
|
||||
unfiltered[1],
|
||||
unfiltered[3],
|
||||
unfiltered[5],
|
||||
unfiltered[6],
|
||||
unfiltered[7],
|
||||
unfiltered[9],
|
||||
unfiltered[12],
|
||||
];
|
||||
|
||||
// verify grid data
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filtered));
|
||||
|
||||
// go to another page and come back
|
||||
await tester.openPage('Getting started');
|
||||
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filtered));
|
||||
|
||||
// delete the filter
|
||||
// TODO(RS): Shouldn't the sort/filter list show automatically!?
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapFilterButtonInGrid('Registration Complete');
|
||||
await tester
|
||||
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(unfiltered));
|
||||
|
||||
// go to another page and come back
|
||||
await tester.openPage('Getting started');
|
||||
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(unfiltered));
|
||||
});
|
||||
|
||||
testWidgets('with both filter and sort configured', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final original = tester.getGridRows();
|
||||
|
||||
// add a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.Checkbox,
|
||||
'Registration Complete',
|
||||
);
|
||||
|
||||
// add a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
final filteredAndSorted = [
|
||||
original[7],
|
||||
original[1],
|
||||
original[9],
|
||||
original[6],
|
||||
original[12],
|
||||
original[3],
|
||||
original[5],
|
||||
];
|
||||
|
||||
// verify grid data
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filteredAndSorted));
|
||||
|
||||
// go to another page and come back
|
||||
await tester.openPage('Getting started');
|
||||
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filteredAndSorted));
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
import 'grid_test_extensions.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid reorder row test:', () {
|
||||
testWidgets('base case', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final original = tester.getGridRows();
|
||||
|
||||
// reorder row
|
||||
await tester.reorderRow(original[4], original[1]);
|
||||
|
||||
// verify grid data
|
||||
List reordered = [
|
||||
original[0],
|
||||
original[4],
|
||||
original[1],
|
||||
original[2],
|
||||
original[3],
|
||||
original[5],
|
||||
original[6],
|
||||
original[7],
|
||||
original[8],
|
||||
original[9],
|
||||
original[10],
|
||||
original[11],
|
||||
original[12],
|
||||
];
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(reordered));
|
||||
|
||||
// reorder row
|
||||
await tester.reorderRow(reordered[1], reordered[3]);
|
||||
|
||||
// verify grid data
|
||||
reordered = [
|
||||
original[0],
|
||||
original[1],
|
||||
original[2],
|
||||
original[4],
|
||||
original[3],
|
||||
original[5],
|
||||
original[6],
|
||||
original[7],
|
||||
original[8],
|
||||
original[9],
|
||||
original[10],
|
||||
original[11],
|
||||
original[12],
|
||||
];
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(reordered));
|
||||
|
||||
// reorder row
|
||||
await tester.reorderRow(reordered[2], reordered[0]);
|
||||
|
||||
// verify grid data
|
||||
reordered = [
|
||||
original[2],
|
||||
original[0],
|
||||
original[1],
|
||||
original[4],
|
||||
original[3],
|
||||
original[5],
|
||||
original[6],
|
||||
original[7],
|
||||
original[8],
|
||||
original[9],
|
||||
original[10],
|
||||
original[11],
|
||||
original[12],
|
||||
];
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(reordered));
|
||||
});
|
||||
|
||||
testWidgets('with active sort', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final original = tester.getGridRows();
|
||||
|
||||
// add a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
// verify grid data
|
||||
final sorted = [
|
||||
original[7],
|
||||
original[8],
|
||||
original[1],
|
||||
original[9],
|
||||
original[11],
|
||||
original[10],
|
||||
original[6],
|
||||
original[12],
|
||||
original[2],
|
||||
original[0],
|
||||
original[3],
|
||||
original[5],
|
||||
original[4],
|
||||
];
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(sorted));
|
||||
|
||||
// reorder row
|
||||
await tester.reorderRow(original[4], original[1]);
|
||||
expect(find.byType(ConfirmPopup), findsOneWidget);
|
||||
await tester.tapButtonWithName(LocaleKeys.button_cancel.tr());
|
||||
|
||||
// verify grid data
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(sorted));
|
||||
});
|
||||
|
||||
testWidgets('with active filter', (tester) async {
|
||||
await tester.openTestDatabase(v069GridFileName);
|
||||
|
||||
// get grid data
|
||||
final original = tester.getGridRows();
|
||||
|
||||
// add a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.Checkbox,
|
||||
'Registration Complete',
|
||||
);
|
||||
|
||||
final filtered = [
|
||||
original[1],
|
||||
original[3],
|
||||
original[5],
|
||||
original[6],
|
||||
original[7],
|
||||
original[9],
|
||||
original[12],
|
||||
];
|
||||
|
||||
// verify grid data
|
||||
List actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(filtered));
|
||||
|
||||
// reorder row
|
||||
await tester.reorderRow(filtered[3], filtered[1]);
|
||||
|
||||
// verify grid data
|
||||
List reordered = [
|
||||
original[1],
|
||||
original[6],
|
||||
original[3],
|
||||
original[5],
|
||||
original[7],
|
||||
original[9],
|
||||
original[12],
|
||||
];
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(reordered));
|
||||
|
||||
// reorder row
|
||||
await tester.reorderRow(reordered[3], reordered[5]);
|
||||
|
||||
// verify grid data
|
||||
reordered = [
|
||||
original[1],
|
||||
original[6],
|
||||
original[3],
|
||||
original[7],
|
||||
original[9],
|
||||
original[5],
|
||||
original[12],
|
||||
];
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(reordered));
|
||||
|
||||
// delete the filter
|
||||
await tester.tapFilterButtonInGrid('Registration Complete');
|
||||
await tester
|
||||
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
|
||||
// verify grid data
|
||||
final expected = [
|
||||
original[0],
|
||||
original[1],
|
||||
original[2],
|
||||
original[6],
|
||||
original[3],
|
||||
original[4],
|
||||
original[7],
|
||||
original[8],
|
||||
original[9],
|
||||
original[5],
|
||||
original[10],
|
||||
original[11],
|
||||
original[12],
|
||||
];
|
||||
actual = tester.getGridRows();
|
||||
expect(actual, orderedEquals(expected));
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
extension GridTestExtensions on WidgetTester {
|
||||
List<RowId> getGridRows() {
|
||||
final databaseController =
|
||||
widget<GridPage>(find.byType(GridPage)).databaseController;
|
||||
return [
|
||||
...databaseController.rowCache.rowInfos.map((e) => e.rowId),
|
||||
];
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/common_operations.dart';
|
||||
import '../../shared/editor_test_operations.dart';
|
||||
import '../../shared/document_test_operations.dart';
|
||||
import '../../shared/expectation.dart';
|
||||
import '../../shared/keyboard.dart';
|
||||
|
||||
|
@ -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_reopen_test.dart' as grid_reopen_test_runner;
|
||||
import 'desktop/grid/grid_create_row_test.dart' as grid_create_row_test_runner;
|
||||
import 'desktop/grid/grid_reorder_row_test.dart'
|
||||
as grid_reorder_row_test_runner;
|
||||
import 'desktop/grid/grid_filter_and_sort_test.dart'
|
||||
as grid_filter_and_sort_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;
|
||||
@ -30,4 +38,9 @@ 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();
|
||||
}
|
||||
|
@ -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,
|
||||
@ -572,8 +654,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 +662,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 +679,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';
|
||||
}
|
@ -70,6 +70,7 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio
|
||||
import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
|
||||
@ -95,8 +96,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 +110,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,6 +570,18 @@ 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();
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> openEmojiPicker() async =>
|
||||
tapButton(find.byType(AddEmojiButton));
|
||||
|
||||
@ -787,7 +798,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 +883,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 +1056,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
/// Must call [tapSortMenuInSettingBar] first.
|
||||
Future<void> tapAllSortButton() async {
|
||||
Future<void> tapDeleteAllSortsButton() async {
|
||||
await tapButton(find.byType(DeleteAllSortsButton));
|
||||
}
|
||||
|
||||
@ -1252,7 +1293,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 +1309,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
);
|
||||
|
||||
await enterText(textField, title);
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
///
|
||||
|
@ -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,57 @@ 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(
|
||||
url: path,
|
||||
uploadType: isLocalMode
|
||||
? FileUploadTypePB.LocalFile
|
||||
: FileUploadTypePB.CloudFile,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
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 +233,37 @@ 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(
|
||||
url: url,
|
||||
uploadType: FileUploadTypePB.NetworkFile,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RowDetailFab extends StatelessWidget {
|
||||
@ -322,91 +408,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.url.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.url,
|
||||
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 +553,9 @@ class _TitleSkin extends IEditableTextCellSkin {
|
||||
fontSize: 23,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
onChanged: (text) => bloc.add(TextCellEvent.updateText(text)),
|
||||
onEditingComplete: () {
|
||||
bloc.add(TextCellEvent.updateText(textEditingController.text));
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 9),
|
||||
border: InputBorder.none,
|
||||
|
@ -90,7 +90,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
|
||||
),
|
||||
onPressed: () {
|
||||
final name = state.content;
|
||||
_openRowPage(context, name);
|
||||
_openRowPage(context, name ?? "");
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1,16 +1,12 @@
|
||||
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/board/application/board_bloc.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:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileCardContent extends StatelessWidget {
|
||||
const MobileCardContent({
|
||||
@ -28,48 +24,30 @@ class MobileCardContent extends StatelessWidget {
|
||||
|
||||
@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,
|
||||
);
|
||||
},
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (rowMeta.cover.url.isNotEmpty) ...[
|
||||
CardCover(
|
||||
cover: rowMeta.cover,
|
||||
userProfile: context.read<BoardBloc>().userProfile,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: styleConfiguration.cardPadding,
|
||||
child: Column(
|
||||
children: [
|
||||
...cells.map(
|
||||
(cellMeta) => cellBuilder.build(
|
||||
cellContext: cellMeta.cellContext(),
|
||||
styleMap: mobileBoardCardCellStyleMap(context),
|
||||
hasNotes: !rowMeta.isDocumentEmpty,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -225,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,
|
||||
),
|
||||
),
|
||||
|
@ -7,6 +7,7 @@ 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';
|
||||
@ -194,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;
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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.url.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.url,
|
||||
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>;
|
||||
|
@ -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,9 +6,11 @@ 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_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@ -47,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;
|
||||
|
||||
@ -94,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
|
||||
|
@ -1,5 +1,8 @@
|
||||
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';
|
||||
@ -23,14 +26,13 @@ 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';
|
||||
@ -687,6 +689,7 @@ class _BoardCardState extends State<_BoardCard> {
|
||||
rowId: rowMeta.id,
|
||||
),
|
||||
),
|
||||
userProfile: context.read<BoardBloc>().userProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -852,6 +855,7 @@ void _openCard({
|
||||
builder: (_) => RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
userProfile: context.read<BoardBloc>().userProfile,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ class CheckboxColumnHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final customData = groupData.customData as GroupData;
|
||||
final groupName = customData.group.generateGroupName(databaseController);
|
||||
return Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
@ -32,9 +33,15 @@ class CheckboxColumnHeader extends StatelessWidget {
|
||||
),
|
||||
const HSpace(6),
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
customData.group.generateGroupName(databaseController),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: FlowyTooltip(
|
||||
message: groupName,
|
||||
child: FlowyText.medium(
|
||||
groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
|
@ -204,12 +204,19 @@ class _DefaultColumnHeaderContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final customData = groupData.customData as GroupData;
|
||||
final groupName = customData.group.generateGroupName(databaseController);
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
customData.group.generateGroupName(databaseController),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: FlowyTooltip(
|
||||
message: groupName,
|
||||
child: FlowyText.medium(
|
||||
groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
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';
|
||||
@ -7,7 +6,6 @@ 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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -111,17 +109,18 @@ class _EditableColumnHeaderState extends State<EditableColumnHeader> {
|
||||
|
||||
Widget _buildTitle() {
|
||||
final (backgroundColor, dotColor) = _generateGroupColor();
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
widget.isEditing.value = true;
|
||||
},
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
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),
|
||||
@ -143,9 +142,12 @@ class _EditableColumnHeaderState extends State<EditableColumnHeader> {
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyText.medium(
|
||||
_generateGroupName(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
groupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -419,12 +419,11 @@ class HiddenGroupPopupItemList extends StatelessWidget {
|
||||
onPressed: () {
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
);
|
||||
},
|
||||
builder: (_) => RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
userProfile: context.read<BoardBloc>().userProfile,
|
||||
),
|
||||
);
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
|
@ -7,6 +7,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
@ -33,11 +34,20 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
CellMemCache get cellCache => databaseController.rowCache.cellCache;
|
||||
RowCache get rowCache => databaseController.rowCache;
|
||||
|
||||
UserProfilePB? _userProfile;
|
||||
UserProfilePB? get userProfile => _userProfile;
|
||||
|
||||
void _dispatch() {
|
||||
on<CalendarEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final result = await UserEventGetUserProfile().send();
|
||||
result.fold(
|
||||
(profile) => _userProfile = profile,
|
||||
(err) => Log.error('Failed to get user profile: $err'),
|
||||
);
|
||||
|
||||
_startListening();
|
||||
await _openDatabase(emit);
|
||||
_loadAllEvents();
|
||||
|
@ -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';
|
||||
@ -10,12 +12,12 @@ 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,6 +82,7 @@ class _EventCardState extends State<EventCard> {
|
||||
rowCache: rowCache,
|
||||
isEditing: false,
|
||||
cellBuilder: cellBuilder,
|
||||
isCompact: true,
|
||||
onTap: (context) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
context.push(
|
||||
@ -107,6 +110,7 @@ class _EventCardState extends State<EventCard> {
|
||||
),
|
||||
onStartEditing: () {},
|
||||
onEndEditing: () {},
|
||||
userProfile: context.read<CalendarBloc>().userProfile,
|
||||
);
|
||||
|
||||
final decoration = BoxDecoration(
|
||||
|
@ -130,6 +130,7 @@ class EventEditorControls extends StatelessWidget {
|
||||
child: RowDetailPage(
|
||||
databaseController: databaseController,
|
||||
rowController: rowController,
|
||||
userProfile: context.read<CalendarBloc>().userProfile,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -287,10 +288,8 @@ class _TitleTextCellSkin extends IEditableTextCellSkin {
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14),
|
||||
focusNode: focusNode,
|
||||
hintText: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
|
||||
onChanged: (text) {
|
||||
if (textEditingController.value.composing.isCollapsed) {
|
||||
bloc.add(TextCellEvent.updateText(text));
|
||||
}
|
||||
onEditingComplete: () {
|
||||
bloc.add(TextCellEvent.updateText(textEditingController.text));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@ -17,13 +19,13 @@ 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';
|
||||
@ -365,6 +367,7 @@ void showEventDetails({
|
||||
child: RowDetailPage(
|
||||
rowController: rowController,
|
||||
databaseController: databaseController,
|
||||
userProfile: context.read<CalendarBloc>().userProfile,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -6,9 +6,11 @@ import 'package:appflowy/plugins/database/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@ -27,11 +29,20 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
|
||||
String get viewId => databaseController.viewId;
|
||||
|
||||
UserProfilePB? _userProfile;
|
||||
UserProfilePB? get userProfile => _userProfile;
|
||||
|
||||
void _dispatch() {
|
||||
on<GridEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final response = await UserEventGetUserProfile().send();
|
||||
response.fold(
|
||||
(userProfile) => _userProfile = userProfile,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
|
||||
_startListening();
|
||||
await _openGrid(emit);
|
||||
},
|
||||
|
@ -1,5 +1,10 @@
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@ -9,17 +14,23 @@ class MobileRowDetailBloc
|
||||
extends Bloc<MobileRowDetailEvent, MobileRowDetailState> {
|
||||
MobileRowDetailBloc({required this.databaseController})
|
||||
: super(MobileRowDetailState.initial()) {
|
||||
rowBackendService = RowBackendService(viewId: databaseController.viewId);
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final DatabaseController databaseController;
|
||||
late final RowBackendService rowBackendService;
|
||||
|
||||
UserProfilePB? _userProfile;
|
||||
UserProfilePB? get userProfile => _userProfile;
|
||||
|
||||
void _dispatch() {
|
||||
on<MobileRowDetailEvent>(
|
||||
(event, emit) {
|
||||
event.when(
|
||||
initial: (rowId) {
|
||||
initial: (rowId) async {
|
||||
_startListening();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
@ -27,6 +38,12 @@ class MobileRowDetailBloc
|
||||
rowInfos: databaseController.rowCache.rowInfos,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await UserEventGetUserProfile().send();
|
||||
result.fold(
|
||||
(profile) => _userProfile = profile,
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
},
|
||||
didLoadRows: (rows) {
|
||||
emit(state.copyWith(rowInfos: rows));
|
||||
@ -34,6 +51,16 @@ class MobileRowDetailBloc
|
||||
changeRowId: (rowId) {
|
||||
emit(state.copyWith(currentRowId: rowId));
|
||||
},
|
||||
addCover: (rowCover) async {
|
||||
if (state.currentRowId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await rowBackendService.updateMeta(
|
||||
rowId: state.currentRowId!,
|
||||
cover: rowCover,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -66,6 +93,7 @@ class MobileRowDetailEvent with _$MobileRowDetailEvent {
|
||||
const factory MobileRowDetailEvent.didLoadRows(List<RowInfo> rows) =
|
||||
_DidLoadRows;
|
||||
const factory MobileRowDetailEvent.changeRowId(String rowId) = _ChangeRowId;
|
||||
const factory MobileRowDetailEvent.addCover(RowCoverPB cover) = _AddCover;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -1,12 +1,17 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_settings_service.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database/domain/row_meta_listener.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@ -16,7 +21,8 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
RowDetailBloc({
|
||||
required this.fieldController,
|
||||
required this.rowController,
|
||||
}) : super(RowDetailState.initial()) {
|
||||
}) : _metaListener = RowMetaListener(rowController.rowId),
|
||||
super(RowDetailState.initial(rowController.rowMeta)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
_init();
|
||||
@ -26,12 +32,14 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
|
||||
final FieldController fieldController;
|
||||
final RowController rowController;
|
||||
final RowMetaListener _metaListener;
|
||||
|
||||
final List<CellContext> allCells = [];
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await rowController.dispose();
|
||||
await _metaListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@ -91,12 +99,27 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
endEditingField: () {
|
||||
emit(state.copyWith(editingFieldId: "", newFieldId: ""));
|
||||
},
|
||||
removeCover: () {
|
||||
RowBackendService(viewId: rowController.viewId)
|
||||
.removeCover(rowController.rowId);
|
||||
},
|
||||
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,11 @@ class RowDetailEvent with _$RowDetailEvent {
|
||||
|
||||
/// End editing an event
|
||||
const factory RowDetailEvent.endEditingField() = _EndEditingField;
|
||||
|
||||
const factory RowDetailEvent.removeCover() = _RemoveCover;
|
||||
|
||||
const factory RowDetailEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
|
||||
_DidReceiveRowMeta;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -249,14 +277,16 @@ class RowDetailState with _$RowDetailState {
|
||||
required int numHiddenFields,
|
||||
required String editingFieldId,
|
||||
required String newFieldId,
|
||||
required RowMetaPB rowMeta,
|
||||
}) = _RowDetailState;
|
||||
|
||||
factory RowDetailState.initial() => const RowDetailState(
|
||||
factory RowDetailState.initial(RowMetaPB rowMeta) => RowDetailState(
|
||||
fields: [],
|
||||
visibleCells: [],
|
||||
showHiddenFields: false,
|
||||
numHiddenFields: 0,
|
||||
editingFieldId: "",
|
||||
newFieldId: "",
|
||||
rowMeta: rowMeta,
|
||||
);
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {
|
||||
// new document for the given document id of the row.
|
||||
final documentView =
|
||||
await _createRowDocumentView(rowMeta.documentId);
|
||||
if (documentView != null) {
|
||||
if (documentView != null && !isClosed) {
|
||||
add(RowDocumentEvent.didReceiveRowDocument(documentView));
|
||||
}
|
||||
} else {
|
||||
|
@ -1,6 +1,10 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
||||
@ -9,11 +13,12 @@ import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -23,6 +28,7 @@ import '../../application/row/row_controller.dart';
|
||||
import '../../tab_bar/tab_bar_view.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
import '../application/grid_bloc.dart';
|
||||
|
||||
import 'grid_scroll.dart';
|
||||
import 'layout/layout.dart';
|
||||
import 'layout/sizes.dart';
|
||||
@ -197,6 +203,7 @@ class _GridPageState extends State<GridPage> {
|
||||
child: RowDetailPage(
|
||||
databaseController: context.read<GridBloc>().databaseController,
|
||||
rowController: rowController,
|
||||
userProfile: context.read<GridBloc>().userProfile,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -293,12 +300,14 @@ class _GridRowsState extends State<_GridRows> {
|
||||
|
||||
void _evaluateFloatingCalculations() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
// maxScrollExtent is 0.0 if scrolling is not possible
|
||||
showFloatingCalculations = widget
|
||||
.scrollController.verticalController.position.maxScrollExtent >
|
||||
0;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// maxScrollExtent is 0.0 if scrolling is not possible
|
||||
showFloatingCalculations = widget.scrollController.verticalController
|
||||
.position.maxScrollExtent >
|
||||
0;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,6 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
@ -8,6 +10,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class RowActionMenu extends StatelessWidget {
|
||||
const RowActionMenu({
|
||||
@ -57,17 +60,7 @@ class RowActionMenu extends StatelessWidget {
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: () {
|
||||
if (action == RowAction.delete) {
|
||||
NavigatorOkCancelDialog(
|
||||
message: LocaleKeys.grid_row_deleteRowPrompt.tr(),
|
||||
onOkPressed: () {
|
||||
action.performAction(context, viewId, rowId);
|
||||
},
|
||||
).show(context);
|
||||
} else {
|
||||
action.performAction(context, viewId, rowId);
|
||||
}
|
||||
|
||||
action.performAction(context, viewId, rowId);
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
leftIcon: icon,
|
||||
@ -107,17 +100,45 @@ enum RowAction {
|
||||
final position = this == insertAbove
|
||||
? OrderObjectPositionTypePB.Before
|
||||
: OrderObjectPositionTypePB.After;
|
||||
RowBackendService.createRow(
|
||||
viewId: viewId,
|
||||
position: position,
|
||||
targetRowId: rowId,
|
||||
);
|
||||
final intention = this == insertAbove
|
||||
? LocaleKeys.grid_row_createRowAboveDescription.tr()
|
||||
: LocaleKeys.grid_row_createRowBelowDescription.tr();
|
||||
if (context.read<GridBloc>().state.sorts.isNotEmpty) {
|
||||
showCancelAndDeleteDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.grid_sort_sortsActive.tr(
|
||||
namedArgs: {'intention': intention},
|
||||
),
|
||||
description: LocaleKeys.grid_sort_removeSorting.tr(),
|
||||
confirmLabel: LocaleKeys.button_remove.tr(),
|
||||
closeOnAction: true,
|
||||
onDelete: () {
|
||||
SortBackendService(viewId: viewId).deleteAllSorts();
|
||||
RowBackendService.createRow(
|
||||
viewId: viewId,
|
||||
position: position,
|
||||
targetRowId: rowId,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
RowBackendService.createRow(
|
||||
viewId: viewId,
|
||||
position: position,
|
||||
targetRowId: rowId,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case duplicate:
|
||||
RowBackendService.duplicateRow(viewId, rowId);
|
||||
break;
|
||||
case delete:
|
||||
RowBackendService.deleteRows(viewId, [rowId]);
|
||||
showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: LocaleKeys.grid_row_label.tr(),
|
||||
description: LocaleKeys.grid_row_deleteRowPrompt.tr(),
|
||||
onConfirm: () => RowBackendService.deleteRows(viewId, [rowId]),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -31,8 +34,7 @@ class GridRow extends StatefulWidget {
|
||||
required this.rowController,
|
||||
required this.cellBuilder,
|
||||
required this.openDetailPage,
|
||||
this.index,
|
||||
this.isDraggable = false,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final FieldController fieldController;
|
||||
@ -41,8 +43,7 @@ class GridRow extends StatefulWidget {
|
||||
final RowController rowController;
|
||||
final EditableCellBuilder cellBuilder;
|
||||
final void Function(BuildContext context) openDetailPage;
|
||||
final int? index;
|
||||
final bool isDraggable;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
State<GridRow> createState() => _GridRowState();
|
||||
@ -62,8 +63,8 @@ class _GridRowState extends State<GridRow> {
|
||||
child: Row(
|
||||
children: [
|
||||
_RowLeading(
|
||||
viewId: widget.viewId,
|
||||
index: widget.index,
|
||||
isDraggable: widget.isDraggable,
|
||||
),
|
||||
Expanded(
|
||||
child: RowContent(
|
||||
@ -81,12 +82,12 @@ class _GridRowState extends State<GridRow> {
|
||||
|
||||
class _RowLeading extends StatefulWidget {
|
||||
const _RowLeading({
|
||||
this.index,
|
||||
this.isDraggable = false,
|
||||
required this.viewId,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final int? index;
|
||||
final bool isDraggable;
|
||||
final String viewId;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
State<_RowLeading> createState() => _RowLeadingState();
|
||||
@ -105,9 +106,12 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
popupBuilder: (_) {
|
||||
final bloc = context.read<RowBloc>();
|
||||
return RowActionMenu(
|
||||
viewId: bloc.viewId,
|
||||
rowId: bloc.rowId,
|
||||
return BlocProvider.value(
|
||||
value: context.read<GridBloc>(),
|
||||
child: RowActionMenu(
|
||||
viewId: bloc.viewId,
|
||||
rowId: bloc.rowId,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Consumer<RegionStateNotifier>(
|
||||
@ -127,28 +131,25 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const InsertRowButton(),
|
||||
if (isDraggable)
|
||||
ReorderableDragStartListener(
|
||||
index: widget.index!,
|
||||
child: RowMenuButton(
|
||||
isDragEnabled: isDraggable,
|
||||
openMenu: popoverController.show,
|
||||
),
|
||||
)
|
||||
else
|
||||
RowMenuButton(
|
||||
InsertRowButton(viewId: widget.viewId),
|
||||
ReorderableDragStartListener(
|
||||
index: widget.index,
|
||||
child: RowMenuButton(
|
||||
openMenu: popoverController.show,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool get isDraggable => widget.index != null && widget.isDraggable;
|
||||
}
|
||||
|
||||
class InsertRowButton extends StatelessWidget {
|
||||
const InsertRowButton({super.key});
|
||||
const InsertRowButton({
|
||||
super.key,
|
||||
required this.viewId,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -157,7 +158,28 @@ class InsertRowButton extends StatelessWidget {
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 20,
|
||||
height: 30,
|
||||
onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
|
||||
onPressed: () {
|
||||
final rowBloc = context.read<RowBloc>();
|
||||
if (context.read<GridBloc>().state.sorts.isNotEmpty) {
|
||||
showCancelAndDeleteDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.grid_sort_sortsActive.tr(
|
||||
namedArgs: {
|
||||
'intention': LocaleKeys.grid_row_createRowBelowDescription.tr(),
|
||||
},
|
||||
),
|
||||
description: LocaleKeys.grid_sort_removeSorting.tr(),
|
||||
confirmLabel: LocaleKeys.button_remove.tr(),
|
||||
closeOnAction: true,
|
||||
onDelete: () {
|
||||
SortBackendService(viewId: viewId).deleteAllSorts();
|
||||
rowBloc.add(const RowEvent.createRow());
|
||||
},
|
||||
);
|
||||
} else {
|
||||
rowBloc.add(const RowEvent.createRow());
|
||||
}
|
||||
},
|
||||
iconPadding: const EdgeInsets.all(3),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
@ -171,11 +193,9 @@ class RowMenuButton extends StatefulWidget {
|
||||
const RowMenuButton({
|
||||
super.key,
|
||||
required this.openMenu,
|
||||
this.isDragEnabled = false,
|
||||
});
|
||||
|
||||
final VoidCallback openMenu;
|
||||
final bool isDragEnabled;
|
||||
|
||||
@override
|
||||
State<RowMenuButton> createState() => _RowMenuButtonState();
|
||||
@ -185,22 +205,18 @@ class _RowMenuButtonState extends State<RowMenuButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
tooltipText:
|
||||
widget.isDragEnabled ? null : LocaleKeys.tooltip_openMenu.tr(),
|
||||
richTooltipText: widget.isDragEnabled
|
||||
? TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.tooltip_dragRow.tr()}\n',
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.tooltip_openMenu.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
richTooltipText: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.tooltip_dragRow.tr()}\n',
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.tooltip_openMenu.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
],
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 20,
|
||||
height: 30,
|
||||
|
@ -2,7 +2,6 @@ import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_accessory_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_menu.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -55,27 +54,22 @@ class _DatabaseViewSettingContent extends StatelessWidget {
|
||||
return BlocBuilder<DatabaseViewSettingExtensionBloc,
|
||||
DatabaseViewSettingExtensionState>(
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.horizontalHeaderPadding,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SortMenu(fieldController: fieldController),
|
||||
const HSpace(6),
|
||||
FilterMenu(fieldController: fieldController),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SortMenu(fieldController: fieldController),
|
||||
const HSpace(6),
|
||||
FilterMenu(fieldController: fieldController),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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,23 +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/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_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.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/material.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';
|
||||
|
||||
import 'card_bloc.dart';
|
||||
import 'container/accessory.dart';
|
||||
import 'container/card_container.dart';
|
||||
@ -39,6 +40,8 @@ class RowCard extends StatefulWidget {
|
||||
this.onShiftTap,
|
||||
this.groupingFieldId,
|
||||
this.groupId,
|
||||
this.userProfile,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
final FieldController fieldController;
|
||||
@ -66,6 +69,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();
|
||||
}
|
||||
@ -165,6 +176,8 @@ class _RowCardState extends State<RowCard> {
|
||||
cellBuilder: widget.cellBuilder,
|
||||
styleConfiguration: widget.styleConfiguration,
|
||||
cells: state.cells,
|
||||
userProfile: widget.userProfile,
|
||||
isCompact: widget.isCompact,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -188,48 +201,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
|
||||
@ -267,6 +267,52 @@ 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!.url.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: AFImage(
|
||||
url: cover!.url,
|
||||
uploadType: cover!.uploadType,
|
||||
userProfile: userProfile,
|
||||
height: isCompact ? 50 : 100,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EditCardAccessory extends StatelessWidget with CardAccessory {
|
||||
const EditCardAccessory({super.key});
|
||||
|
||||
|
@ -28,7 +28,7 @@ class RowCardContainer extends StatelessWidget {
|
||||
create: (_) => _CardContainerNotifier(),
|
||||
child: Consumer<_CardContainerNotifier>(
|
||||
builder: (context, notifier, _) {
|
||||
Widget container = Center(child: child);
|
||||
Widget container = child;
|
||||
bool shouldBuildAccessory = true;
|
||||
if (buildAccessoryWhen != null) {
|
||||
shouldBuildAccessory = buildAccessoryWhen!.call();
|
||||
@ -52,7 +52,7 @@ class RowCardContainer extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
constraints: const BoxConstraints(minHeight: 42),
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
|
@ -1,8 +1,17 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/card_cell.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MediaCardCellStyle extends CardCellStyle {
|
||||
const MediaCardCellStyle({
|
||||
@ -13,9 +22,6 @@ class MediaCardCellStyle extends CardCellStyle {
|
||||
final TextStyle textStyle;
|
||||
}
|
||||
|
||||
// This is a placeholder for the MediaCardCell, it is not implemented
|
||||
// as we use the [RowMetaPB.attachmentCount] to display cumulative attachments
|
||||
// on a Card.
|
||||
class MediaCardCell extends CardCell<MediaCardCellStyle> {
|
||||
const MediaCardCell({
|
||||
super.key,
|
||||
@ -34,6 +40,42 @@ class MediaCardCell extends CardCell<MediaCardCellStyle> {
|
||||
class _MediaCellState extends State<MediaCardCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox.shrink();
|
||||
return BlocProvider<MediaCellBloc>(
|
||||
create: (_) => MediaCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
)..add(const MediaCellEvent.initial()),
|
||||
child: BlocBuilder<MediaCellBloc, MediaCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.files.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.media_s,
|
||||
size: Size.square(12),
|
||||
),
|
||||
const HSpace(6),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.grid_media_attachmentsHint
|
||||
.tr(args: ['${state.files.length}']),
|
||||
fontSize: 12,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -122,9 +122,7 @@ class _TextCellState extends State<TextCardCell> {
|
||||
child: BlocListener<TextCellBloc, TextCellState>(
|
||||
listenWhen: (previous, current) => previous.content != current.content,
|
||||
listener: (context, state) {
|
||||
if (!state.enableEdit) {
|
||||
_textEditingController.text = state.content;
|
||||
}
|
||||
_textEditingController.text = state.content ?? "";
|
||||
},
|
||||
child: isTitle ? _buildTitle() : _buildText(),
|
||||
),
|
||||
@ -143,9 +141,9 @@ class _TextCellState extends State<TextCardCell> {
|
||||
|
||||
Widget? _buildIcon(TextCellState state) {
|
||||
if (state.emoji?.value.isNotEmpty ?? false) {
|
||||
return Text(
|
||||
return FlowyText.emoji(
|
||||
optimizeEmojiAlign: true,
|
||||
state.emoji?.value ?? '',
|
||||
style: widget.style.titleTextStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@ -164,7 +162,7 @@ class _TextCellState extends State<TextCardCell> {
|
||||
Widget _buildText() {
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final content = state.content;
|
||||
final content = state.content ?? "";
|
||||
|
||||
return content.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
@ -187,6 +185,7 @@ class _TextCellState extends State<TextCardCell> {
|
||||
builder: (context, state) {
|
||||
final icon = _buildIcon(state);
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
icon,
|
||||
@ -224,8 +223,7 @@ class _TextCellState extends State<TextCardCell> {
|
||||
enableInteractiveSelection: isEditing,
|
||||
style: widget.style.titleTextStyle,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: widget.style.padding
|
||||
.add(const EdgeInsets.symmetric(vertical: 4.0)),
|
||||
contentPadding: widget.style.padding,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@ -9,13 +9,12 @@ 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/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:universal_platform/universal_platform.dart';
|
||||
|
||||
@ -160,38 +159,16 @@ class _FilePreviewRender extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child;
|
||||
if (file.fileType == MediaFileTypePB.Image) {
|
||||
if (file.uploadType == MediaUploadTypePB.NetworkMedia) {
|
||||
child = Image.network(
|
||||
file.url,
|
||||
height: 32,
|
||||
width: 32,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else if (file.uploadType == MediaUploadTypePB.LocalMedia) {
|
||||
child = Image.file(
|
||||
File(file.url),
|
||||
height: 32,
|
||||
width: 32,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else {
|
||||
// Cloud
|
||||
child = FlowyNetworkImage(
|
||||
url: file.url,
|
||||
userProfilePB: context.read<MediaCellBloc>().state.userProfile,
|
||||
height: 32,
|
||||
width: 32,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
child = Container(
|
||||
if (file.fileType != MediaFileTypePB.Image) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
height: 32,
|
||||
width: 32,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).greyHover,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FlowySvg(
|
||||
file.fileType.icon,
|
||||
@ -201,12 +178,18 @@ class _FilePreviewRender extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
height: 32,
|
||||
width: 32,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: child,
|
||||
child: AFImage(
|
||||
url: file.url,
|
||||
uploadType: file.uploadType,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ class DesktopGridTextCellSkin extends IEditableTextCellSkin {
|
||||
return Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _IconOrEmoji(),
|
||||
Expanded(
|
||||
@ -29,6 +30,7 @@ class DesktopGridTextCellSkin extends IEditableTextCellSkin {
|
||||
maxLines: context.watch<TextCellBloc>().state.wrap ? null : 1,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.only(top: 4),
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
@ -52,39 +54,38 @@ class _IconOrEmoji extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (state.emoji != null)
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: state.emoji!,
|
||||
builder: (context, value, child) {
|
||||
if (value.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return FlowyText(
|
||||
value,
|
||||
fontSize: 16,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (state.hasDocument != null)
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: state.hasDocument!,
|
||||
builder: (context, hasDocument, child) {
|
||||
if ((state.emoji?.value.isEmpty ?? true) && hasDocument) {
|
||||
return FlowySvg(
|
||||
FlowySvgs.notes_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
const HSpace(6),
|
||||
],
|
||||
// if not a title cell, return empty widget
|
||||
if (state.emoji == null || state.hasDocument == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ValueListenableBuilder<String>(
|
||||
valueListenable: state.emoji!,
|
||||
builder: (context, emoji, _) {
|
||||
return emoji.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 6.0),
|
||||
child: FlowyText.emoji(
|
||||
optimizeEmojiAlign: true,
|
||||
emoji,
|
||||
),
|
||||
)
|
||||
: ValueListenableBuilder<bool>(
|
||||
valueListenable: state.hasDocument!,
|
||||
builder: (context, hasDocument, _) {
|
||||
return hasDocument
|
||||
? Padding(
|
||||
padding:
|
||||
const EdgeInsetsDirectional.only(end: 6.0),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.notes_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -13,12 +11,13 @@ 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/file_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@ -195,8 +194,8 @@ class _AddFileButton extends StatelessWidget {
|
||||
url: path,
|
||||
name: file.name,
|
||||
uploadType: isLocalMode
|
||||
? MediaUploadTypePB.LocalMedia
|
||||
: MediaUploadTypePB.CloudMedia,
|
||||
? FileUploadTypePB.LocalFile
|
||||
: FileUploadTypePB.CloudFile,
|
||||
fileType: file.fileType.toMediaFileTypePB(),
|
||||
),
|
||||
);
|
||||
@ -229,7 +228,7 @@ class _AddFileButton extends StatelessWidget {
|
||||
MediaCellEvent.addFile(
|
||||
url: url,
|
||||
name: name,
|
||||
uploadType: MediaUploadTypePB.NetworkMedia,
|
||||
uploadType: FileUploadTypePB.NetworkFile,
|
||||
fileType: fileType,
|
||||
),
|
||||
);
|
||||
@ -297,17 +296,11 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
|
||||
Widget build(BuildContext context) {
|
||||
Widget child;
|
||||
if (file.fileType == MediaFileTypePB.Image) {
|
||||
if (file.uploadType == MediaUploadTypePB.NetworkMedia) {
|
||||
child = Image.network(file.url, fit: BoxFit.cover);
|
||||
} else if (file.uploadType == MediaUploadTypePB.LocalMedia) {
|
||||
child = Image.file(File(file.url), fit: BoxFit.cover);
|
||||
} else {
|
||||
// Cloud
|
||||
child = FlowyNetworkImage(
|
||||
url: file.url,
|
||||
userProfilePB: context.read<MediaCellBloc>().state.userProfile,
|
||||
);
|
||||
}
|
||||
child = AFImage(
|
||||
url: file.url,
|
||||
uploadType: file.uploadType,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
);
|
||||
} else {
|
||||
child = DecoratedBox(
|
||||
decoration: BoxDecoration(color: file.fileType.color),
|
||||
|
@ -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,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';
|
||||
@ -115,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(),
|
||||
),
|
||||
);
|
||||
@ -152,7 +151,7 @@ class _MediaCellEditorState extends State<MediaCellEditor> {
|
||||
MediaCellEvent.addFile(
|
||||
url: url,
|
||||
name: name,
|
||||
uploadType: MediaUploadTypePB.NetworkMedia,
|
||||
uploadType: FileUploadTypePB.NetworkFile,
|
||||
fileType: fileType,
|
||||
),
|
||||
);
|
||||
@ -193,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,
|
||||
};
|
||||
}
|
||||
@ -265,39 +264,20 @@ class _RenderMediaState extends State<RenderMedia> {
|
||||
child: const FlowySvg(FlowySvgs.drag_element_s),
|
||||
),
|
||||
const HSpace(8),
|
||||
if (file.fileType == MediaFileTypePB.Image &&
|
||||
file.uploadType == MediaUploadTypePB.CloudMedia) ...[
|
||||
if (widget.file.fileType == MediaFileTypePB.Image) ...[
|
||||
Expanded(
|
||||
child: _openInteractiveViewer(
|
||||
context,
|
||||
files: widget.images,
|
||||
index: imageIndex!,
|
||||
child: FlowyNetworkImage(
|
||||
url: file.url,
|
||||
userProfilePB:
|
||||
child: AFImage(
|
||||
url: widget.file.url,
|
||||
uploadType: widget.file.uploadType,
|
||||
userProfile:
|
||||
context.read<MediaCellBloc>().state.userProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (file.fileType == MediaFileTypePB.Image) ...[
|
||||
Expanded(
|
||||
child: _openInteractiveViewer(
|
||||
context,
|
||||
files: widget.images,
|
||||
index: imageIndex!,
|
||||
child: file.uploadType == MediaUploadTypePB.NetworkMedia
|
||||
? Image.network(
|
||||
file.url,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.centerLeft,
|
||||
)
|
||||
: Image.file(
|
||||
File(file.url),
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
|
@ -14,6 +14,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/comm
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
|
||||
import 'package:appflowy/util/xfile_ext.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:cross_file/cross_file.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -92,8 +93,8 @@ class MobileMediaCellEditor extends StatelessWidget {
|
||||
url: path,
|
||||
name: file.name,
|
||||
uploadType: isLocalMode
|
||||
? MediaUploadTypePB.LocalMedia
|
||||
: MediaUploadTypePB.CloudMedia,
|
||||
? FileUploadTypePB.LocalFile
|
||||
: FileUploadTypePB.CloudFile,
|
||||
fileType:
|
||||
file.fileType.toMediaFileTypePB(),
|
||||
),
|
||||
@ -166,7 +167,7 @@ class MobileMediaCellEditor extends StatelessWidget {
|
||||
MediaCellEvent.addFile(
|
||||
url: url,
|
||||
name: name,
|
||||
uploadType: MediaUploadTypePB.NetworkMedia,
|
||||
uploadType: FileUploadTypePB.NetworkFile,
|
||||
fileType: fileType,
|
||||
),
|
||||
);
|
||||
|
@ -454,17 +454,15 @@ class SelectOptionTagCell extends StatelessWidget {
|
||||
onTap: onSelected,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Align(
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: SelectOptionTag(
|
||||
fontSize: 14,
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: SelectOptionTag(
|
||||
fontSize: 14,
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -27,35 +27,25 @@ class MediaTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
final typeOption = _parseTypeOptionData(field.typeOptionData);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
height: GridSize.popoverItemHeight,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyButton(
|
||||
resetHoverOnRebuild: false,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_media_hideFileNames.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
rightIcon: Toggle(
|
||||
value: typeOption.hideFileNames,
|
||||
onChanged: (value) {
|
||||
onTypeOptionUpdated(
|
||||
_toggleHideFiles(typeOption, !value).writeToBuffer(),
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: FlowyButton(
|
||||
resetHoverOnRebuild: false,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_media_hideFileNames.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onHover: (_) => popoverMutex.close(),
|
||||
rightIcon: Toggle(
|
||||
value: typeOption.hideFileNames,
|
||||
onChanged: (value) {
|
||||
onTypeOptionUpdated(
|
||||
_toggleHideFiles(typeOption, !value).writeToBuffer(),
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -13,7 +13,12 @@ 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_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
|
||||
import 'package:appflowy/shared/af_image.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -28,12 +33,14 @@ class RowBanner extends StatefulWidget {
|
||||
required this.rowController,
|
||||
required this.cellBuilder,
|
||||
this.allowOpenAsFullPage = true,
|
||||
this.userProfile,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final RowController rowController;
|
||||
final EditableCellBuilder cellBuilder;
|
||||
final bool allowOpenAsFullPage;
|
||||
final UserProfilePB? userProfile;
|
||||
|
||||
@override
|
||||
State<RowBanner> createState() => _RowBannerState();
|
||||
@ -57,28 +64,45 @@ class _RowBannerState extends State<RowBanner> {
|
||||
fieldController: widget.databaseController.fieldController,
|
||||
rowMeta: widget.rowController.rowMeta,
|
||||
)..add(const RowBannerEvent.initial()),
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => _isHovering.value = true,
|
||||
onExit: (event) => _isHovering.value = false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(60, 34, 60, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: _BannerAction(
|
||||
isHovering: _isHovering,
|
||||
popoverController: popoverController,
|
||||
child: Builder(
|
||||
builder: (context) => MouseRegion(
|
||||
onEnter: (event) => _isHovering.value = true,
|
||||
onExit: (event) => _isHovering.value = false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BannerCover(userProfile: widget.userProfile),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
60,
|
||||
context.watch<RowBannerBloc>().hasCover ? 4 : 34,
|
||||
60,
|
||||
0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: _BannerAction(
|
||||
rowId: widget.rowController.rowId,
|
||||
isHovering: _isHovering,
|
||||
popoverController: popoverController,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
_BannerTitle(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
popoverController: popoverController,
|
||||
rowController: widget.rowController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(4),
|
||||
_BannerTitle(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
popoverController: popoverController,
|
||||
rowController: widget.rowController,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -86,23 +110,32 @@ class _RowBannerState extends State<RowBanner> {
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerAction extends StatelessWidget {
|
||||
class _BannerAction extends StatefulWidget {
|
||||
const _BannerAction({
|
||||
required this.isHovering,
|
||||
required this.popoverController,
|
||||
required this.rowId,
|
||||
});
|
||||
|
||||
final ValueNotifier<bool> isHovering;
|
||||
final PopoverController popoverController;
|
||||
final String rowId;
|
||||
|
||||
@override
|
||||
State<_BannerAction> createState() => _BannerActionState();
|
||||
}
|
||||
|
||||
class _BannerActionState extends State<_BannerAction> {
|
||||
bool isSelected = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: _kBannerActionHeight,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: isHovering,
|
||||
valueListenable: widget.isHovering,
|
||||
builder: (BuildContext context, bool isHovering, Widget? child) {
|
||||
if (!isHovering) {
|
||||
if (!isHovering && !isSelected) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@ -113,7 +146,7 @@ class _BannerAction extends StatelessWidget {
|
||||
children: [
|
||||
if (state.rowMeta.icon.isEmpty)
|
||||
AddEmojiButton(
|
||||
onTap: () => popoverController.show(),
|
||||
onTap: () => widget.popoverController.show(),
|
||||
)
|
||||
else
|
||||
RemoveEmojiButton(
|
||||
@ -121,6 +154,24 @@ class _BannerAction extends StatelessWidget {
|
||||
.read<RowBannerBloc>()
|
||||
.add(const RowBannerEvent.setIcon('')),
|
||||
),
|
||||
const HSpace(8),
|
||||
if (state.rowMeta.cover.url.isEmpty)
|
||||
AddCoverButton(
|
||||
rowId: widget.rowId,
|
||||
onPopoverChanged: (isShowing) {
|
||||
isSelected = isShowing;
|
||||
|
||||
if (!isShowing) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
)
|
||||
else
|
||||
RemoveCoverButton(
|
||||
onTap: () => context
|
||||
.read<RowBannerBloc>()
|
||||
.add(const RowBannerEvent.removeCover()),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -184,6 +235,48 @@ class _BannerTitle extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class BannerCover extends StatelessWidget {
|
||||
const BannerCover({super.key, required this.userProfile});
|
||||
|
||||
final UserProfilePB? userProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||
buildWhen: (prev, curr) =>
|
||||
prev.rowMeta.cover.url != curr.rowMeta.cover.url,
|
||||
builder: (context, state) {
|
||||
final cover = state.rowMeta.cover;
|
||||
if (cover.url.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 250,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: AFImage(
|
||||
url: cover.url,
|
||||
uploadType: cover.uploadType,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiButton extends StatelessWidget {
|
||||
const EmojiButton({
|
||||
super.key,
|
||||
@ -211,6 +304,87 @@ class EmojiButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AddCoverButton extends StatelessWidget {
|
||||
const AddCoverButton({
|
||||
super.key,
|
||||
required this.rowId,
|
||||
required this.onPopoverChanged,
|
||||
});
|
||||
|
||||
final String rowId;
|
||||
final void Function(bool) onPopoverChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 5),
|
||||
onOpen: () => onPopoverChanged(true),
|
||||
onClose: () => onPopoverChanged(false),
|
||||
popupBuilder: (_) => BlocProvider.value(
|
||||
value: context.read<RowBannerBloc>(),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return UploadImageMenu(
|
||||
supportTypes: const [UploadImageType.local, UploadImageType.url],
|
||||
onSelectedLocalImages: (images) async {
|
||||
if (images.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final image = images.first;
|
||||
await insertLocalFile(
|
||||
context,
|
||||
image,
|
||||
userProfile: context.read<RowBannerBloc>().userProfile,
|
||||
documentId: rowId,
|
||||
onUploadSuccess: (url, isLocalMode) {
|
||||
context.read<RowBannerBloc>().add(
|
||||
RowBannerEvent.setCover(
|
||||
RowCoverPB(
|
||||
url: url,
|
||||
uploadType: isLocalMode
|
||||
? FileUploadTypePB.LocalFile
|
||||
: FileUploadTypePB.CloudFile,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
onPopoverChanged(false);
|
||||
},
|
||||
onSelectedNetworkImage: (String url) {
|
||||
context.read<RowBannerBloc>().add(
|
||||
RowBannerEvent.setCover(
|
||||
RowCoverPB(
|
||||
url: url,
|
||||
uploadType: FileUploadTypePB.NetworkFile,
|
||||
),
|
||||
),
|
||||
);
|
||||
onPopoverChanged(false);
|
||||
},
|
||||
onSelectedAIImage: (_) {},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 26,
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.image_s),
|
||||
margin: const EdgeInsets.all(4),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddEmojiButton extends StatelessWidget {
|
||||
const AddEmojiButton({super.key, required this.onTap});
|
||||
|
||||
@ -234,6 +408,29 @@ class AddEmojiButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveCoverButton extends StatelessWidget {
|
||||
const RemoveCoverButton({super.key, required this.onTap});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.document_plugins_cover_removeCover.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.image_s),
|
||||
onTap: onTap,
|
||||
margin: const EdgeInsets.all(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveEmojiButton extends StatelessWidget {
|
||||
const RemoveEmojiButton({super.key, required this.onTap});
|
||||
|
||||
@ -313,10 +510,8 @@ class _TitleSkin extends IEditableTextCellSkin {
|
||||
isDense: true,
|
||||
isCollapsed: true,
|
||||
),
|
||||
onChanged: (text) {
|
||||
if (textEditingController.value.composing.isCollapsed) {
|
||||
bloc.add(TextCellEvent.updateText(text));
|
||||
}
|
||||
onEditingComplete: () {
|
||||
bloc.add(TextCellEvent.updateText(textEditingController.text));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
@ -10,9 +12,9 @@ import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.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 '../cell/editable_cell_builder.dart';
|
||||
@ -26,11 +28,13 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
||||
required this.rowController,
|
||||
required this.databaseController,
|
||||
this.allowOpenAsFullPage = true,
|
||||
this.userProfile,
|
||||
});
|
||||
|
||||
final RowController rowController;
|
||||
final DatabaseController databaseController;
|
||||
final bool allowOpenAsFullPage;
|
||||
final UserProfilePB? userProfile;
|
||||
|
||||
@override
|
||||
State<RowDetailPage> createState() => _RowDetailPageState();
|
||||
@ -71,6 +75,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
||||
rowController: widget.rowController,
|
||||
cellBuilder: cellBuilder,
|
||||
allowOpenAsFullPage: widget.allowOpenAsFullPage,
|
||||
userProfile: widget.userProfile,
|
||||
),
|
||||
const VSpace(16),
|
||||
Padding(
|
||||
|
@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/row/row_document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
@ -8,8 +11,8 @@ import 'package:appflowy/workspace/application/view_info/view_info_bloc.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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RowDocument extends StatelessWidget {
|
||||
const RowDocument({
|
||||
@ -65,56 +68,69 @@ class _RowEditor extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => DocumentBloc(documentId: viewPB.id)
|
||||
..add(const DocumentEvent.initial()),
|
||||
child: BlocListener<DocumentBloc, DocumentState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isDocumentEmpty != current.isDocumentEmpty,
|
||||
listener: (_, state) {
|
||||
if (state.isDocumentEmpty != null) {
|
||||
onIsEmptyChanged?.call(state.isDocumentEmpty!);
|
||||
}
|
||||
if (state.error != null) {
|
||||
Log.error('RowEditor error: ${state.error}');
|
||||
}
|
||||
if (state.editorState == null) {
|
||||
Log.error('RowEditor unable to get editorState');
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
return ChangeNotifierProvider(
|
||||
// Due to how DropTarget works, there is no way to differentiate if an overlay is
|
||||
// blocking the target visibly, so when we have an overlay with a drop target,
|
||||
// we should disable the drop target for the Editor, until it is closed.
|
||||
//
|
||||
// See FileBlockComponent for sample use.
|
||||
//
|
||||
// Relates to:
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/2
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/331
|
||||
//
|
||||
create: (_) => EditorDropManagerState(),
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentBloc(documentId: viewPB.id)
|
||||
..add(const DocumentEvent.initial()),
|
||||
child: BlocConsumer<DocumentBloc, DocumentState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isDocumentEmpty != current.isDocumentEmpty,
|
||||
listener: (_, state) {
|
||||
if (state.isDocumentEmpty != null) {
|
||||
onIsEmptyChanged?.call(state.isDocumentEmpty!);
|
||||
}
|
||||
if (state.error != null) {
|
||||
Log.error('RowEditor error: ${state.error}');
|
||||
}
|
||||
if (state.editorState == null) {
|
||||
Log.error('RowEditor unable to get editorState');
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
final editorState = state.editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
return Center(
|
||||
child: AppFlowyErrorPage(
|
||||
error: error,
|
||||
),
|
||||
child: AppFlowyErrorPage(error: error),
|
||||
);
|
||||
}
|
||||
|
||||
return BlocProvider<ViewInfoBloc>(
|
||||
create: (context) => ViewInfoBloc(view: viewPB),
|
||||
child: IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 300),
|
||||
child: AppFlowyEditorPage(
|
||||
shrinkWrap: true,
|
||||
autoFocus: false,
|
||||
editorState: editorState,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: const EdgeInsets.only(left: 16, right: 54),
|
||||
return Consumer<EditorDropManagerState>(
|
||||
builder: (_, dropState, __) => BlocProvider<ViewInfoBloc>(
|
||||
create: (context) => ViewInfoBloc(view: viewPB),
|
||||
child: IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 300),
|
||||
child: AppFlowyEditorPage(
|
||||
shrinkWrap: true,
|
||||
autoFocus: false,
|
||||
editorState: editorState,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: const EdgeInsets.only(left: 16, right: 54),
|
||||
),
|
||||
showParagraphPlaceholder: (editorState, node) =>
|
||||
editorState.document.isEmpty,
|
||||
placeholderText: (node) =>
|
||||
LocaleKeys.cardDetails_notesPlaceholder.tr(),
|
||||
),
|
||||
showParagraphPlaceholder: (editorState, node) =>
|
||||
editorState.document.isEmpty,
|
||||
placeholderText: (node) =>
|
||||
LocaleKeys.cardDetails_notesPlaceholder.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -107,7 +107,6 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
|
||||
editorState: state.editorState!,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
// the 44 is the width of the left action list
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
),
|
||||
header: _buildDatabaseDataContent(context, state.editorState!),
|
||||
@ -137,6 +136,7 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
|
||||
return state.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
ready: (databaseController, rowController) {
|
||||
final padding = EditorStyleCustomizer.documentPadding;
|
||||
return BlocProvider(
|
||||
create: (context) => RowDetailBloc(
|
||||
fieldController: databaseController.fieldController,
|
||||
@ -145,8 +145,8 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 24,
|
||||
left: EditorStyleCustomizer.documentPadding.left + 16 + 6,
|
||||
right: EditorStyleCustomizer.documentPadding.right,
|
||||
left: padding.left + 22,
|
||||
right: padding.right,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -145,7 +145,7 @@ class _TitleSkin extends IEditableTextCellSkin {
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return BlocSelector<TextCellBloc, TextCellState, String>(
|
||||
selector: (state) => state.content,
|
||||
selector: (state) => state.content ?? "",
|
||||
builder: (context, content) {
|
||||
final name = content.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
|
@ -1,17 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/util/color_to_hex_string.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
class DocumentAppearance {
|
||||
const DocumentAppearance({
|
||||
required this.fontSize,
|
||||
required this.fontFamily,
|
||||
required this.codeFontFamily,
|
||||
required this.width,
|
||||
this.cursorColor,
|
||||
this.selectionColor,
|
||||
this.defaultTextDirection,
|
||||
@ -23,6 +26,7 @@ class DocumentAppearance {
|
||||
final Color? cursorColor;
|
||||
final Color? selectionColor;
|
||||
final String? defaultTextDirection;
|
||||
final double width;
|
||||
|
||||
/// For nullable fields (like `cursorColor`),
|
||||
/// use the corresponding `isNull` flag (like `cursorColorIsNull`) to explicitly set the field to `null`.
|
||||
@ -39,6 +43,7 @@ class DocumentAppearance {
|
||||
bool cursorColorIsNull = false,
|
||||
bool selectionColorIsNull = false,
|
||||
bool textDirectionIsNull = false,
|
||||
double? width,
|
||||
}) {
|
||||
return DocumentAppearance(
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
@ -50,6 +55,7 @@ class DocumentAppearance {
|
||||
defaultTextDirection: textDirectionIsNull
|
||||
? null
|
||||
: defaultTextDirection ?? this.defaultTextDirection,
|
||||
width: width ?? this.width,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -57,10 +63,13 @@ class DocumentAppearance {
|
||||
class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
||||
DocumentAppearanceCubit()
|
||||
: super(
|
||||
const DocumentAppearance(
|
||||
DocumentAppearance(
|
||||
fontSize: 16.0,
|
||||
fontFamily: defaultFontFamily,
|
||||
codeFontFamily: builtInCodeFontFamily,
|
||||
width: UniversalPlatform.isMobile
|
||||
? double.infinity
|
||||
: EditorStyleCustomizer.maxDocumentWidth,
|
||||
),
|
||||
);
|
||||
|
||||
@ -82,6 +91,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
||||
final selectionColor = selectionColorString != null
|
||||
? Color(int.parse(selectionColorString))
|
||||
: null;
|
||||
final double? width = prefs.getDouble(KVKeys.kDocumentAppearanceWidth);
|
||||
|
||||
final textScaleFactor =
|
||||
double.parse(prefs.getString(KVKeys.textScaleFactor) ?? '1.0');
|
||||
@ -100,6 +110,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
||||
cursorColorIsNull: cursorColor == null,
|
||||
selectionColorIsNull: selectionColor == null,
|
||||
textDirectionIsNull: defaultTextDirection == null,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -186,4 +197,21 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> syncWidth(double? width) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
width ??= UniversalPlatform.isMobile
|
||||
? double.infinity
|
||||
: EditorStyleCustomizer.maxDocumentWidth;
|
||||
width = width.clamp(
|
||||
EditorStyleCustomizer.minDocumentWidth,
|
||||
EditorStyleCustomizer.maxDocumentWidth,
|
||||
);
|
||||
await prefs.setDouble(KVKeys.kDocumentAppearanceWidth, width);
|
||||
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(width: width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
|
||||
@ -145,6 +146,8 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
DocumentState state,
|
||||
EditorDropManagerState dropState,
|
||||
) {
|
||||
final width = context.read<DocumentAppearanceCubit>().state.width;
|
||||
|
||||
final Widget child;
|
||||
if (UniversalPlatform.isMobile) {
|
||||
child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
|
||||
@ -153,7 +156,7 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
editorState: state.editorState!,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
// the 44 is the width of the left action list
|
||||
width: width,
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
),
|
||||
header: _buildCoverAndIcon(context, state),
|
||||
@ -171,7 +174,7 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
.getDropTargetRenderData(details.globalPosition);
|
||||
|
||||
if (data != null &&
|
||||
data.dropTarget != null &&
|
||||
data.dropPath != null &&
|
||||
|
||||
// We implement custom Drop logic for image blocks, this is
|
||||
// how we can exclude them from the Drop Target
|
||||
@ -184,14 +187,28 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
}
|
||||
},
|
||||
onDragDone: (details) async {
|
||||
state.editorState!.selectionService.removeDropTarget();
|
||||
final editorState = state.editorState;
|
||||
if (editorState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final data = state.editorState!.selectionService
|
||||
editorState.selectionService.removeDropTarget();
|
||||
|
||||
final data = editorState.selectionService
|
||||
.getDropTargetRenderData(details.globalPosition);
|
||||
|
||||
if (data != null) {
|
||||
if (data.cursorNode != null) {
|
||||
if (_excludeFromDropTarget.contains(data.cursorNode?.type)) {
|
||||
final cursorNode = data.cursorNode;
|
||||
final dropPath = data.dropPath;
|
||||
|
||||
if (cursorNode != null && dropPath != null) {
|
||||
if (_excludeFromDropTarget.contains(cursorNode.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(dropPath);
|
||||
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -209,14 +226,15 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
}
|
||||
}
|
||||
|
||||
await editorState!.dropImages(
|
||||
data.dropTarget!,
|
||||
await editorState.dropImages(
|
||||
node,
|
||||
imageFiles,
|
||||
widget.view.id,
|
||||
isLocalMode,
|
||||
);
|
||||
await editorState!.dropFiles(
|
||||
data.dropTarget!,
|
||||
|
||||
await editorState.dropFiles(
|
||||
node,
|
||||
otherFiles,
|
||||
widget.view.id,
|
||||
isLocalMode,
|
||||
@ -228,7 +246,7 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
editorState: state.editorState!,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
// the 44 is the width of the left action list
|
||||
width: width,
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
),
|
||||
header: _buildCoverAndIcon(context, state),
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
@ -12,8 +15,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
@ -221,6 +222,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),
|
||||
languagePickerBuilder: codeBlockLanguagePickerBuilder,
|
||||
copyButtonBuilder: codeBlockCopyBuilder,
|
||||
showLineNumbers: false,
|
||||
),
|
||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||
@ -314,6 +316,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: editorState,
|
||||
blockComponentBuilder: builders,
|
||||
actions: actions,
|
||||
showSlashMenu: slashMenuItems != null
|
||||
? () => customSlashCommand(
|
||||
|
@ -150,6 +150,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
||||
// code block
|
||||
formatBacktickToCodeBlock,
|
||||
...codeBlockCharacterEvents,
|
||||
|
||||
// callout block
|
||||
@ -235,7 +236,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
_initEditorL10n();
|
||||
_initializeShortcuts();
|
||||
appFlowyEditorAutoScrollEdgeOffset = 220;
|
||||
|
||||
indentableBlockTypes.add(ToggleListBlockKeys.type);
|
||||
convertibleBlockTypes.add(ToggleListBlockKeys.type);
|
||||
slashMenuItems = _customSlashMenuItems();
|
||||
|
@ -11,34 +11,42 @@ class BlockActionButton extends StatelessWidget {
|
||||
required this.svg,
|
||||
required this.richMessage,
|
||||
required this.onTap,
|
||||
this.showTooltip = true,
|
||||
});
|
||||
|
||||
final FlowySvgData svg;
|
||||
final bool showTooltip;
|
||||
final InlineSpan richMessage;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
child: FlowyTooltip(
|
||||
richMessage: richMessage,
|
||||
child: MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grab,
|
||||
child: IgnoreParentGestureWidget(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: FlowySvg(
|
||||
svg,
|
||||
size: const Size.square(18.0),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
Widget child = MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grab,
|
||||
child: IgnoreParentGestureWidget(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
child: FlowySvg(
|
||||
svg,
|
||||
size: const Size.square(18.0),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (showTooltip) {
|
||||
child = FlowyTooltip(
|
||||
richMessage: richMessage,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return Align(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
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:flutter/material.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BlockActionList extends StatelessWidget {
|
||||
const BlockActionList({
|
||||
@ -12,6 +13,7 @@ class BlockActionList extends StatelessWidget {
|
||||
required this.editorState,
|
||||
required this.actions,
|
||||
required this.showSlashMenu,
|
||||
required this.blockComponentBuilder,
|
||||
});
|
||||
|
||||
final BlockComponentContext blockComponentContext;
|
||||
@ -19,6 +21,7 @@ class BlockActionList extends StatelessWidget {
|
||||
final List<OptionAction> actions;
|
||||
final VoidCallback showSlashMenu;
|
||||
final EditorState editorState;
|
||||
final Map<String, BlockComponentBuilder> blockComponentBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -31,14 +34,15 @@ class BlockActionList extends StatelessWidget {
|
||||
editorState: editorState,
|
||||
showSlashMenu: showSlashMenu,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
const HSpace(4.0),
|
||||
BlockOptionButton(
|
||||
blockComponentContext: blockComponentContext,
|
||||
blockComponentState: blockComponentState,
|
||||
actions: actions,
|
||||
editorState: editorState,
|
||||
blockComponentBuilder: blockComponentBuilder,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
const HSpace(4.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,48 +1,59 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.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 'package:provider/provider.dart';
|
||||
|
||||
class BlockOptionButton extends StatelessWidget {
|
||||
import 'drag_to_reorder/draggable_option_button.dart';
|
||||
|
||||
class BlockOptionButton extends StatefulWidget {
|
||||
const BlockOptionButton({
|
||||
super.key,
|
||||
required this.blockComponentContext,
|
||||
required this.blockComponentState,
|
||||
required this.actions,
|
||||
required this.editorState,
|
||||
required this.blockComponentBuilder,
|
||||
});
|
||||
|
||||
final BlockComponentContext blockComponentContext;
|
||||
final BlockComponentActionState blockComponentState;
|
||||
final List<OptionAction> actions;
|
||||
final EditorState editorState;
|
||||
final Map<String, BlockComponentBuilder> blockComponentBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final popoverActions = actions.map((e) {
|
||||
State<BlockOptionButton> createState() => _BlockOptionButtonState();
|
||||
}
|
||||
|
||||
class _BlockOptionButtonState extends State<BlockOptionButton> {
|
||||
late final List<PopoverAction> popoverActions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
popoverActions = widget.actions.map((e) {
|
||||
switch (e) {
|
||||
case OptionAction.divider:
|
||||
return DividerOptionAction();
|
||||
case OptionAction.color:
|
||||
return ColorOptionAction(editorState: editorState);
|
||||
return ColorOptionAction(editorState: widget.editorState);
|
||||
case OptionAction.align:
|
||||
return AlignOptionAction(editorState: editorState);
|
||||
return AlignOptionAction(editorState: widget.editorState);
|
||||
case OptionAction.depth:
|
||||
return DepthOptionAction(editorState: editorState);
|
||||
return DepthOptionAction(editorState: widget.editorState);
|
||||
default:
|
||||
return OptionActionWrapper(e);
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopoverActionList<PopoverAction>(
|
||||
popoverMutex: PopoverMutex(),
|
||||
direction:
|
||||
@ -53,13 +64,13 @@ class BlockOptionButton extends StatelessWidget {
|
||||
actions: popoverActions,
|
||||
onPopupBuilder: () {
|
||||
keepEditorFocusNotifier.increase();
|
||||
blockComponentState.alwaysShowActions = true;
|
||||
widget.blockComponentState.alwaysShowActions = true;
|
||||
},
|
||||
onClosed: () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
editorState.selectionType = null;
|
||||
editorState.selection = null;
|
||||
blockComponentState.alwaysShowActions = false;
|
||||
widget.editorState.selectionType = null;
|
||||
widget.editorState.selection = null;
|
||||
widget.blockComponentState.alwaysShowActions = false;
|
||||
keepEditorFocusNotifier.decrease();
|
||||
});
|
||||
},
|
||||
@ -69,62 +80,18 @@ class BlockOptionButton extends StatelessWidget {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
buildChild: (controller) => _buildOptionButton(context, controller),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionButton(
|
||||
BuildContext context,
|
||||
PopoverController controller,
|
||||
) {
|
||||
return BlockActionButton(
|
||||
svg: FlowySvgs.drag_element_s,
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
// todo: customize the color to highlight the text.
|
||||
text: LocaleKeys.document_plugins_optionAction_click.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
],
|
||||
buildChild: (controller) => DraggableOptionButton(
|
||||
controller: controller,
|
||||
editorState: widget.editorState,
|
||||
blockComponentContext: widget.blockComponentContext,
|
||||
blockComponentBuilder: widget.blockComponentBuilder,
|
||||
),
|
||||
onTap: () {
|
||||
controller.show();
|
||||
|
||||
// update selection
|
||||
_updateBlockSelection();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateBlockSelection() {
|
||||
final startNode = blockComponentContext.node;
|
||||
var endNode = startNode;
|
||||
while (endNode.children.isNotEmpty) {
|
||||
endNode = endNode.children.last;
|
||||
}
|
||||
|
||||
final start = Position(path: startNode.path);
|
||||
final end = endNode.selectable?.end() ??
|
||||
Position(
|
||||
path: endNode.path,
|
||||
offset: endNode.delta?.length ?? 0,
|
||||
);
|
||||
|
||||
editorState.selectionType = SelectionType.block;
|
||||
editorState.selection = Selection(
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
}
|
||||
|
||||
void _onSelectAction(BuildContext context, OptionAction action) {
|
||||
final node = blockComponentContext.node;
|
||||
final transaction = editorState.transaction;
|
||||
final node = widget.blockComponentContext.node;
|
||||
final transaction = widget.editorState.transaction;
|
||||
switch (action) {
|
||||
case OptionAction.delete:
|
||||
transaction.deleteNode(node);
|
||||
@ -146,7 +113,7 @@ class BlockOptionButton extends StatelessWidget {
|
||||
case OptionAction.depth:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
editorState.apply(transaction);
|
||||
widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
void _duplicateBlock(
|
||||
@ -156,8 +123,7 @@ class BlockOptionButton extends StatelessWidget {
|
||||
) {
|
||||
// 1. verify the node integrity
|
||||
final type = node.type;
|
||||
final builder =
|
||||
context.read<EditorState>().renderer.blockComponentBuilder(type);
|
||||
final builder = widget.editorState.renderer.blockComponentBuilder(type);
|
||||
|
||||
if (builder == null) {
|
||||
Log.error('Block type $type is not supported');
|
||||
@ -184,8 +150,7 @@ class BlockOptionButton extends StatelessWidget {
|
||||
Node copiedNode = node.copyWith();
|
||||
|
||||
final type = node.type;
|
||||
final builder =
|
||||
context.read<EditorState>().renderer.blockComponentBuilder(type);
|
||||
final builder = widget.editorState.renderer.blockComponentBuilder(type);
|
||||
|
||||
if (builder == null) {
|
||||
Log.error('Block type $type is not supported');
|
||||
|
@ -0,0 +1,319 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.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:provider/provider.dart';
|
||||
|
||||
// this flag is used to disable the tooltip of the block when it is dragged
|
||||
@visibleForTesting
|
||||
ValueNotifier<bool> isDraggingAppFlowyEditorBlock = ValueNotifier(false);
|
||||
|
||||
class DraggableOptionButton extends StatefulWidget {
|
||||
const DraggableOptionButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.editorState,
|
||||
required this.blockComponentContext,
|
||||
required this.blockComponentBuilder,
|
||||
});
|
||||
|
||||
final PopoverController controller;
|
||||
final EditorState editorState;
|
||||
final BlockComponentContext blockComponentContext;
|
||||
final Map<String, BlockComponentBuilder> blockComponentBuilder;
|
||||
@override
|
||||
State<DraggableOptionButton> createState() => _DraggableOptionButtonState();
|
||||
}
|
||||
|
||||
class _DraggableOptionButtonState extends State<DraggableOptionButton> {
|
||||
late Node node;
|
||||
late BlockComponentContext blockComponentContext;
|
||||
|
||||
Offset? globalPosition;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// copy the node to avoid the node in document being updated
|
||||
node = widget.blockComponentContext.node.copyWith();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
node.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable<Node>(
|
||||
data: node,
|
||||
onDragStarted: _onDragStart,
|
||||
onDragUpdate: _onDragUpdate,
|
||||
onDragEnd: _onDragEnd,
|
||||
feedback: _OptionButtonFeedback(
|
||||
controller: widget.controller,
|
||||
editorState: widget.editorState,
|
||||
blockComponentContext: widget.blockComponentContext,
|
||||
blockComponentBuilder: widget.blockComponentBuilder,
|
||||
),
|
||||
child: _OptionButton(
|
||||
isDragging: isDraggingAppFlowyEditorBlock,
|
||||
controller: widget.controller,
|
||||
editorState: widget.editorState,
|
||||
blockComponentContext: widget.blockComponentContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDragStart() {
|
||||
isDraggingAppFlowyEditorBlock.value = true;
|
||||
widget.editorState.selectionService.removeDropTarget();
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
isDraggingAppFlowyEditorBlock.value = true;
|
||||
|
||||
widget.editorState.selectionService.renderDropTargetForOffset(
|
||||
details.globalPosition,
|
||||
builder: (context, data) {
|
||||
return VisualDragArea(
|
||||
data: data,
|
||||
dragNode: widget.blockComponentContext.node,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
globalPosition = details.globalPosition;
|
||||
|
||||
// auto scroll the page when the drag position is at the edge of the screen
|
||||
widget.editorState.scrollService?.startAutoScroll(
|
||||
details.localPosition,
|
||||
);
|
||||
}
|
||||
|
||||
void _onDragEnd(DraggableDetails details) {
|
||||
isDraggingAppFlowyEditorBlock.value = false;
|
||||
|
||||
widget.editorState.selectionService.removeDropTarget();
|
||||
|
||||
if (globalPosition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final data = widget.editorState.selectionService.getDropTargetRenderData(
|
||||
globalPosition!,
|
||||
);
|
||||
dragToMoveNode(
|
||||
context,
|
||||
node: widget.blockComponentContext.node,
|
||||
acceptedPath: data?.cursorNode?.path,
|
||||
dragOffset: globalPosition!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionButtonFeedback extends StatefulWidget {
|
||||
const _OptionButtonFeedback({
|
||||
required this.controller,
|
||||
required this.editorState,
|
||||
required this.blockComponentContext,
|
||||
required this.blockComponentBuilder,
|
||||
});
|
||||
|
||||
final PopoverController controller;
|
||||
final EditorState editorState;
|
||||
final BlockComponentContext blockComponentContext;
|
||||
final Map<String, BlockComponentBuilder> blockComponentBuilder;
|
||||
|
||||
@override
|
||||
State<_OptionButtonFeedback> createState() => _OptionButtonFeedbackState();
|
||||
}
|
||||
|
||||
class _OptionButtonFeedbackState extends State<_OptionButtonFeedback> {
|
||||
late Node node;
|
||||
late BlockComponentContext blockComponentContext;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_setupLockComponentContext();
|
||||
widget.blockComponentContext.node.addListener(_updateBlockComponentContext);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.blockComponentContext.node
|
||||
.removeListener(_updateBlockComponentContext);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxWidth = (widget.editorState.renderBox?.size.width ??
|
||||
MediaQuery.of(context).size.width) *
|
||||
0.8;
|
||||
|
||||
return Opacity(
|
||||
opacity: 0.7,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Provider.value(
|
||||
value: widget.editorState,
|
||||
child: _buildBlock(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBlock() {
|
||||
final node = widget.blockComponentContext.node;
|
||||
final builder = widget.blockComponentBuilder[node.type];
|
||||
if (builder == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
const unsupportedRenderBlockTypes = [
|
||||
TableBlockKeys.type,
|
||||
CustomImageBlockKeys.type,
|
||||
MultiImageBlockKeys.type,
|
||||
FileBlockKeys.type,
|
||||
DatabaseBlockKeys.boardType,
|
||||
DatabaseBlockKeys.calendarType,
|
||||
DatabaseBlockKeys.gridType,
|
||||
];
|
||||
|
||||
if (unsupportedRenderBlockTypes.contains(node.type)) {
|
||||
// unable to render table block without provider/context
|
||||
// render a placeholder instead
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: FlowyText(node.type.replaceAll('_', ' ').capitalize()),
|
||||
);
|
||||
}
|
||||
|
||||
return IntrinsicHeight(
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: widget.editorState),
|
||||
Provider.value(value: getIt<ReminderBloc>()),
|
||||
],
|
||||
child: builder.build(blockComponentContext),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateBlockComponentContext() {
|
||||
setState(() => _setupLockComponentContext());
|
||||
}
|
||||
|
||||
void _setupLockComponentContext() {
|
||||
node = widget.blockComponentContext.node.copyWith();
|
||||
blockComponentContext = BlockComponentContext(
|
||||
widget.blockComponentContext.buildContext,
|
||||
node,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionButton extends StatelessWidget {
|
||||
const _OptionButton({
|
||||
required this.controller,
|
||||
required this.editorState,
|
||||
required this.blockComponentContext,
|
||||
required this.isDragging,
|
||||
});
|
||||
|
||||
final PopoverController controller;
|
||||
final EditorState editorState;
|
||||
final BlockComponentContext blockComponentContext;
|
||||
final ValueNotifier<bool> isDragging;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: isDragging,
|
||||
builder: (context, isDragging, child) {
|
||||
return BlockActionButton(
|
||||
svg: FlowySvgs.drag_element_s,
|
||||
showTooltip: !isDragging,
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: LocaleKeys.document_plugins_optionAction_drag.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.document_plugins_optionAction_toMove.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: LocaleKeys.document_plugins_optionAction_click.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
controller.show();
|
||||
|
||||
// update selection
|
||||
_updateBlockSelection();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateBlockSelection() {
|
||||
final startNode = blockComponentContext.node;
|
||||
var endNode = startNode;
|
||||
while (endNode.children.isNotEmpty) {
|
||||
endNode = endNode.children.last;
|
||||
}
|
||||
|
||||
final start = Position(path: startNode.path);
|
||||
final end = endNode.selectable?.end() ??
|
||||
Position(
|
||||
path: endNode.path,
|
||||
offset: endNode.delta?.length ?? 0,
|
||||
);
|
||||
|
||||
editorState.selectionType = SelectionType.block;
|
||||
editorState.selection = Selection(
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
enum HorizontalPosition { left, center, right }
|
||||
|
||||
enum VerticalPosition { top, middle, bottom }
|
||||
|
||||
Future<void> dragToMoveNode(
|
||||
BuildContext context, {
|
||||
required Node node,
|
||||
required Offset dragOffset,
|
||||
Path? acceptedPath,
|
||||
}) async {
|
||||
if (acceptedPath == null) {
|
||||
Log.info('acceptedPath is null');
|
||||
return;
|
||||
}
|
||||
|
||||
final editorState = context.read<EditorState>();
|
||||
final targetNode = editorState.getNodeAtPath(acceptedPath);
|
||||
if (targetNode == null) {
|
||||
Log.info('targetNode is null');
|
||||
return;
|
||||
}
|
||||
|
||||
final position = getDragAreaPosition(context, targetNode, dragOffset);
|
||||
if (position == null) {
|
||||
Log.info('position is null');
|
||||
return;
|
||||
}
|
||||
|
||||
final (verticalPosition, horizontalPosition, _) = position;
|
||||
Path newPath = targetNode.path;
|
||||
|
||||
// Determine the new path based on drop position
|
||||
// For VerticalPosition.top, we keep the target node's path
|
||||
if (verticalPosition == VerticalPosition.bottom) {
|
||||
newPath = horizontalPosition == HorizontalPosition.left
|
||||
? newPath.next // Insert after target node
|
||||
: newPath.child(0); // Insert as first child of target node
|
||||
}
|
||||
|
||||
// Check if the drop should be ignored
|
||||
if (shouldIgnoreDragTarget(node, newPath)) {
|
||||
Log.info(
|
||||
'Drop ignored: node($node, ${node.path}), path($acceptedPath)',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info('Moving node($node, ${node.path}) to path($newPath)');
|
||||
|
||||
// Perform the node move operation
|
||||
final transaction = editorState.transaction;
|
||||
transaction.deleteNode(node);
|
||||
transaction.insertNode(newPath, node.copyWith());
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
(VerticalPosition, HorizontalPosition, Rect)? getDragAreaPosition(
|
||||
BuildContext context,
|
||||
Node dragTargetNode,
|
||||
Offset dragOffset,
|
||||
) {
|
||||
final selectable = dragTargetNode.selectable;
|
||||
final renderBox = selectable?.context.findRenderObject() as RenderBox?;
|
||||
if (selectable == null || renderBox == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// disable the table cell block
|
||||
if (dragTargetNode.parent?.type == TableCellBlockKeys.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final globalBlockOffset = renderBox.localToGlobal(Offset.zero);
|
||||
final globalBlockRect = globalBlockOffset & renderBox.size;
|
||||
|
||||
// Check if the dragOffset is within the globalBlockRect
|
||||
final isInside = globalBlockRect.contains(dragOffset);
|
||||
|
||||
if (!isInside) {
|
||||
Log.info(
|
||||
'the drag offset is not inside the block, dragOffset($dragOffset), globalBlockRect($globalBlockRect)',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine the relative position
|
||||
HorizontalPosition horizontalPosition = HorizontalPosition.left;
|
||||
VerticalPosition verticalPosition;
|
||||
|
||||
// Horizontal position
|
||||
if (dragOffset.dx < globalBlockRect.left + 88) {
|
||||
horizontalPosition = HorizontalPosition.left;
|
||||
} else if (indentableBlockTypes.contains(dragTargetNode.type)) {
|
||||
// For indentable blocks, it means the block can contain a child block.
|
||||
// ignore the middle here, it's not used in this example
|
||||
horizontalPosition = HorizontalPosition.right;
|
||||
}
|
||||
|
||||
// Vertical position
|
||||
if (dragOffset.dy < globalBlockRect.top + globalBlockRect.height / 2) {
|
||||
verticalPosition = VerticalPosition.top;
|
||||
} else {
|
||||
verticalPosition = VerticalPosition.bottom;
|
||||
}
|
||||
|
||||
return (verticalPosition, horizontalPosition, globalBlockRect);
|
||||
}
|
||||
|
||||
bool shouldIgnoreDragTarget(Node dragNode, Path? targetPath) {
|
||||
if (targetPath == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dragNode.path.equals(targetPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dragNode.path.isAncestorOf(targetPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
class VisualDragArea extends StatelessWidget {
|
||||
const VisualDragArea({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.dragNode,
|
||||
});
|
||||
|
||||
final DragAreaBuilderData data;
|
||||
final Node dragNode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final targetNode = data.targetNode;
|
||||
|
||||
final ignore = shouldIgnoreDragTarget(dragNode, targetNode.path);
|
||||
if (ignore) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final selectable = targetNode.selectable;
|
||||
final renderBox = selectable?.context.findRenderObject() as RenderBox?;
|
||||
if (selectable == null || renderBox == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final position = getDragAreaPosition(
|
||||
context,
|
||||
targetNode,
|
||||
data.dragOffset,
|
||||
);
|
||||
|
||||
if (position == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final (verticalPosition, horizontalPosition, globalBlockRect) = position;
|
||||
|
||||
// 44 is the width of the drag indicator
|
||||
const indicatorWidth = 44.0;
|
||||
final width = globalBlockRect.width - indicatorWidth;
|
||||
|
||||
Widget child = Container(
|
||||
height: 2,
|
||||
width: width,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
|
||||
if (horizontalPosition == HorizontalPosition.right) {
|
||||
const breakWidth = 22.0;
|
||||
const padding = 8.0;
|
||||
child = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 2,
|
||||
width: breakWidth,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: padding),
|
||||
Container(
|
||||
height: 2,
|
||||
width: width - breakWidth - padding,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
top: verticalPosition == VerticalPosition.top
|
||||
? globalBlockRect.top
|
||||
: globalBlockRect.bottom,
|
||||
// 44 is the width of the drag indicator
|
||||
left: globalBlockRect.left + 44,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
|
||||
/// ``` to code block
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - mobile
|
||||
/// - web
|
||||
///
|
||||
final CharacterShortcutEvent formatBacktickToCodeBlock = CharacterShortcutEvent(
|
||||
key: '``` to code block',
|
||||
character: '`',
|
||||
handler: (editorState) async => _convertBacktickToCodeBlock(
|
||||
editorState: editorState,
|
||||
),
|
||||
);
|
||||
|
||||
Future<bool> _convertBacktickToCodeBlock({
|
||||
required EditorState editorState,
|
||||
}) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null || delta.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only active when the backtick is at the beginning of the line
|
||||
final plainText = delta.toPlainText();
|
||||
if (plainText != '``') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode(
|
||||
selection.end.path,
|
||||
codeBlockNode(),
|
||||
);
|
||||
transaction.deleteNode(node);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(path: selection.start.path),
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
|
||||
return true;
|
||||
}
|
@ -77,7 +77,7 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
child: BlocConsumer<DocumentImmersiveCoverBloc,
|
||||
DocumentImmersiveCoverState>(
|
||||
listener: (context, state) {
|
||||
if (textEditingController.text.isEmpty) {
|
||||
if (textEditingController.text != state.name) {
|
||||
textEditingController.text = state.name;
|
||||
}
|
||||
},
|
||||
|
@ -14,6 +14,7 @@ import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/dispatch/error.dart';
|
||||
import 'package:appflowy_backend/log.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/auth.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
@ -101,8 +102,8 @@ Future<void> downloadMediaFile(
|
||||
UserProfilePB? userProfile,
|
||||
}) async {
|
||||
if ([
|
||||
MediaUploadTypePB.NetworkMedia,
|
||||
MediaUploadTypePB.LocalMedia,
|
||||
FileUploadTypePB.NetworkFile,
|
||||
FileUploadTypePB.LocalFile,
|
||||
].contains(file.uploadType)) {
|
||||
/// When the file is a network file or a local file, we can directly open the file.
|
||||
await afLaunchUrl(Uri.parse(file.url));
|
||||
|
@ -1,10 +1,12 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
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/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/desktop_cover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
@ -94,6 +96,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
PageStyleCover? cover;
|
||||
late ViewPB view;
|
||||
late final ViewListener viewListener;
|
||||
int retryCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -123,50 +126,91 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _reload() => setState(() {});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _calculateOverallHeight(),
|
||||
child: DocumentHeaderToolbar(
|
||||
onIconOrCoverChanged: _saveIconOrCover,
|
||||
node: widget.node,
|
||||
editorState: widget.editorState,
|
||||
hasCover: hasCover,
|
||||
hasIcon: hasIcon,
|
||||
),
|
||||
),
|
||||
if (hasCover)
|
||||
DocumentCover(
|
||||
view: view,
|
||||
editorState: widget.editorState,
|
||||
node: widget.node,
|
||||
coverType: coverType,
|
||||
coverDetails: coverDetails,
|
||||
onChangeCover: (type, details) =>
|
||||
_saveIconOrCover(cover: (type, details)),
|
||||
),
|
||||
if (hasIcon)
|
||||
Positioned(
|
||||
left: UniversalPlatform.isDesktopOrWeb ? 80 : 20,
|
||||
// if hasCover, there shouldn't be icons present so the icon can
|
||||
// be closer to the bottom.
|
||||
bottom:
|
||||
hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight,
|
||||
child: DocumentIcon(
|
||||
editorState: widget.editorState,
|
||||
node: widget.node,
|
||||
icon: viewIcon,
|
||||
onChangeIcon: (icon) => _saveIconOrCover(icon: icon),
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final offset = _calculateIconLeft(context, constraints);
|
||||
return Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _calculateOverallHeight(),
|
||||
child: DocumentHeaderToolbar(
|
||||
onIconOrCoverChanged: _saveIconOrCover,
|
||||
node: widget.node,
|
||||
editorState: widget.editorState,
|
||||
hasCover: hasCover,
|
||||
hasIcon: hasIcon,
|
||||
offset: offset,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (hasCover)
|
||||
DocumentCover(
|
||||
view: view,
|
||||
editorState: widget.editorState,
|
||||
node: widget.node,
|
||||
coverType: coverType,
|
||||
coverDetails: coverDetails,
|
||||
onChangeCover: (type, details) =>
|
||||
_saveIconOrCover(cover: (type, details)),
|
||||
),
|
||||
// don't render the icon if the offset is 0
|
||||
if (hasIcon && offset != 0)
|
||||
Positioned(
|
||||
left: offset,
|
||||
// if hasCover, there shouldn't be icons present so the icon can
|
||||
// be closer to the bottom.
|
||||
bottom: hasCover
|
||||
? kToolbarHeight - kIconHeight / 2
|
||||
: kToolbarHeight,
|
||||
child: DocumentIcon(
|
||||
editorState: widget.editorState,
|
||||
node: widget.node,
|
||||
icon: viewIcon,
|
||||
onChangeIcon: (icon) => _saveIconOrCover(icon: icon),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _reload() => setState(() {});
|
||||
|
||||
double _calculateIconLeft(BuildContext context, BoxConstraints constraints) {
|
||||
final editorState = context.read<EditorState>();
|
||||
final appearanceCubit = context.read<DocumentAppearanceCubit>();
|
||||
|
||||
final renderBox = editorState.renderBox;
|
||||
|
||||
if (renderBox == null || !renderBox.hasSize) {}
|
||||
|
||||
var renderBoxWidth = 0.0;
|
||||
if (renderBox != null && renderBox.hasSize) {
|
||||
renderBoxWidth = renderBox.size.width;
|
||||
} else if (retryCount <= 3) {
|
||||
retryCount++;
|
||||
// this is a workaround for the issue that the renderBox is not initialized
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_reload();
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
// if the renderBox width equals to 0, it means the editor is not initialized
|
||||
final editorWidth = renderBoxWidth != 0
|
||||
? min(renderBoxWidth, appearanceCubit.state.width)
|
||||
: appearanceCubit.state.width;
|
||||
|
||||
// left padding + editor width + right padding = the width of the editor
|
||||
final leftOffset = (constraints.maxWidth - editorWidth) / 2.0 +
|
||||
EditorStyleCustomizer.documentPadding.right;
|
||||
|
||||
// ensure the offset is not negative
|
||||
return max(0, leftOffset);
|
||||
}
|
||||
|
||||
double _calculateOverallHeight() {
|
||||
switch ((hasIcon, hasCover)) {
|
||||
case (true, true):
|
||||
@ -223,6 +267,7 @@ class DocumentHeaderToolbar extends StatefulWidget {
|
||||
required this.hasCover,
|
||||
required this.hasIcon,
|
||||
required this.onIconOrCoverChanged,
|
||||
required this.offset,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
@ -231,6 +276,7 @@ class DocumentHeaderToolbar extends StatefulWidget {
|
||||
final bool hasIcon;
|
||||
final void Function({(CoverType, String?)? cover, String? icon})
|
||||
onIconOrCoverChanged;
|
||||
final double offset;
|
||||
|
||||
@override
|
||||
State<DocumentHeaderToolbar> createState() => _DocumentHeaderToolbarState();
|
||||
@ -254,13 +300,7 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
||||
Widget child = Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
width: double.infinity,
|
||||
padding: UniversalPlatform.isDesktopOrWeb
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: EditorStyleCustomizer.documentPadding.right,
|
||||
)
|
||||
: EdgeInsets.symmetric(
|
||||
horizontal: EditorStyleCustomizer.documentPadding.left,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: widget.offset),
|
||||
child: SizedBox(
|
||||
height: 28,
|
||||
child: Row(
|
||||
@ -484,11 +524,16 @@ class DocumentCoverState extends State<DocumentCover> {
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImages: (paths) async {
|
||||
onSelectedLocalImages: (files) async {
|
||||
context.pop();
|
||||
|
||||
if (files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onChangeCover(
|
||||
CoverType.file,
|
||||
paths.first,
|
||||
files.first.path,
|
||||
);
|
||||
},
|
||||
onSelectedAIImage: (_) {
|
||||
@ -613,9 +658,14 @@ class DocumentCoverState extends State<DocumentCover> {
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImages: (paths) {
|
||||
onSelectedLocalImages: (files) {
|
||||
popoverController.close();
|
||||
onCoverChanged(CoverType.file, paths.first);
|
||||
if (files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final item = files.map((file) => file.path).first;
|
||||
onCoverChanged(CoverType.file, item);
|
||||
},
|
||||
onSelectedAIImage: (_) {
|
||||
throw UnimplementedError();
|
||||
|
@ -1,5 +1,7 @@
|
||||
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/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@ -22,7 +24,6 @@ import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart';
|
||||
@ -103,11 +104,13 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImages: (paths) {
|
||||
onSelectedLocalImages: (files) {
|
||||
controller.close();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final List<String> items = List.from(
|
||||
paths.where((url) => url != null && url.isNotEmpty),
|
||||
files
|
||||
.where((file) => file.path.isNotEmpty)
|
||||
.map((file) => file.path),
|
||||
);
|
||||
if (items.isNotEmpty) {
|
||||
await insertMultipleLocalImages(items);
|
||||
@ -224,12 +227,13 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImages: (paths) async {
|
||||
onSelectedLocalImages: (files) async {
|
||||
context.pop();
|
||||
|
||||
final List<String> items = List.from(
|
||||
paths.where((url) => url != null && url.isNotEmpty),
|
||||
);
|
||||
final items = files
|
||||
.where((file) => file.path.isNotEmpty)
|
||||
.map((file) => file.path)
|
||||
.toList();
|
||||
|
||||
await insertMultipleLocalImages(items);
|
||||
},
|
||||
@ -251,6 +255,10 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
Future<void> insertMultipleLocalImages(List<String> urls) async {
|
||||
controller.close();
|
||||
|
||||
if (urls.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
showLoading = true;
|
||||
errorMessage = null;
|
||||
@ -259,10 +267,6 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
bool hasError = false;
|
||||
|
||||
if (_isLocalMode()) {
|
||||
if (urls.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final first = urls.removeAt(0);
|
||||
final firstPath = await saveImageToLocalStorage(first);
|
||||
final transaction = editorState.transaction;
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/document/application/document_bloc.dart';
|
||||
@ -17,13 +20,12 @@ import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:cross_file/cross_file.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/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:provider/provider.dart';
|
||||
@ -276,11 +278,16 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertLocalImages(List<String?> urls) async {
|
||||
Future<void> insertLocalImages(List<XFile> files) async {
|
||||
controller.close();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (urls.isEmpty || urls.every((path) => path?.isEmpty ?? true)) {
|
||||
final urls = files
|
||||
.map((file) => file.path)
|
||||
.where((path) => path.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
if (urls.isEmpty || urls.every((url) => url.isEmpty)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -331,7 +338,7 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
|
||||
|
||||
final response = await get(uri);
|
||||
await File(copyToPath).writeAsBytes(response.bodyBytes);
|
||||
await insertLocalImages([copyToPath]);
|
||||
await insertLocalImages([XFile(copyToPath)]);
|
||||
await File(copyToPath).delete();
|
||||
} catch (e) {
|
||||
Log.error('cannot save image file', e);
|
||||
|
@ -108,9 +108,10 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImages: (paths) {
|
||||
onSelectedLocalImages: (files) {
|
||||
controller.close();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final paths = files.map((file) => file.path).toList();
|
||||
await insertLocalImages(paths);
|
||||
});
|
||||
},
|
||||
@ -191,9 +192,10 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
],
|
||||
onSelectedLocalImages: (paths) async {
|
||||
onSelectedLocalImages: (files) async {
|
||||
context.pop();
|
||||
await insertLocalImages(paths);
|
||||
final items = files.map((file) => file.path).toList();
|
||||
await insertLocalImages(items);
|
||||
},
|
||||
onSelectedAIImage: (url) async {
|
||||
context.pop();
|
||||
|
@ -1,8 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
|
||||
@ -10,6 +8,7 @@ import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
@ -61,7 +60,7 @@ class _ResizableImageState extends State<ResizableImage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageWidth = widget.width;
|
||||
_userProfilePB = context.read<DocumentBloc>().state.userProfilePB;
|
||||
_userProfilePB = context.read<DocumentBloc?>()?.state.userProfilePB;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsp
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption;
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -42,7 +43,7 @@ class UploadImageMenu extends StatefulWidget {
|
||||
this.allowMultipleImages = false,
|
||||
});
|
||||
|
||||
final void Function(List<String?>) onSelectedLocalImages;
|
||||
final void Function(List<XFile>) onSelectedLocalImages;
|
||||
final void Function(String url) onSelectedAIImage;
|
||||
final void Function(String url) onSelectedNetworkImage;
|
||||
final void Function(String color)? onSelectedColor;
|
||||
|
@ -19,7 +19,7 @@ class UploadImageFileWidget extends StatelessWidget {
|
||||
this.allowMultipleImages = false,
|
||||
});
|
||||
|
||||
final void Function(List<String?>) onPickFiles;
|
||||
final void Function(List<XFile>) onPickFiles;
|
||||
final List<String> allowedExtensions;
|
||||
final bool allowMultipleImages;
|
||||
|
||||
@ -59,7 +59,7 @@ class UploadImageFileWidget extends StatelessWidget {
|
||||
allowedExtensions: allowedExtensions,
|
||||
allowMultiple: allowMultipleImages,
|
||||
);
|
||||
onPickFiles(result?.files.map((f) => f.path).toList() ?? const []);
|
||||
onPickFiles(result?.files.map((f) => f.xFile).toList() ?? const []);
|
||||
} else {
|
||||
final photoPermission =
|
||||
await PermissionChecker.checkPhotoPermission(context);
|
||||
@ -69,7 +69,7 @@ class UploadImageFileWidget extends StatelessWidget {
|
||||
}
|
||||
// on mobile, the users can pick a image file from camera or image library
|
||||
final result = await ImagePicker().pickMultiImage();
|
||||
onPickFiles(result.map((f) => f.path).toList());
|
||||
onPickFiles(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,3 +58,4 @@ export 'table/table_option_action.dart';
|
||||
export 'todo_list/todo_list_icon.dart';
|
||||
export 'toggle/toggle_block_component.dart';
|
||||
export 'toggle/toggle_block_shortcut_event.dart';
|
||||
export 'base/backtick_character_command.dart';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user