Merge branch 'main' into main

This commit is contained in:
KD-MM2 2024-09-23 00:32:19 +09:00 committed by GitHub
commit 9076296748
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
471 changed files with 14272 additions and 4690 deletions

View File

@ -1,4 +1,30 @@
# Release Notes
## Version 0.7.0 - 19/09/2024
### New Features
- Support reordering blocks in document with drag and drop
- Support for adding a cover to a row/card in databases
- Added support for accessing settings on the sign-in page
- Added "Move to" option to the document menu in top right corner
- Support for adjusting the document width from settings
- Show full name of a group on hover
- Colored group names in kanban boards
- Support "Ask AI" on multiple lines of text
- Support for keyboard gestures to move cursor on Mobile
- Added markdown support for quickly inserting a code block using three backticks
### Bug Fixes
- Fixed a critical bug where the backtick character would crash the application
- Fixed an issue with signing-in from the settings dialog where the dialog would persist
- Fixed a visual bug with icon alignment in primary cell of database rows
- Fixed a bug with filters applied where new rows were inserted in wrong position
- Fixed a bug where "Untitled" would override the name of the row
- Fixed page title not updating after renaming from "More"-menu
- Fixed File block breaking row detail document
- Fixed issues with reordering rows with sorting rules applied
- Improvements to the File & Media type in Database
- Performance improvement in Grid view
- Fixed filters sometimes not applying properly in databases
## Version 0.6.9 - 09/09/2024
### New Features
- Added a new property type, 'Files & media'

View File

@ -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""}"

View File

@ -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');
});
});
}

View File

@ -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();
}

View File

@ -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);
});
});
}

View File

@ -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);
});
});
}

View File

@ -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);
},
);
});
});
}

View File

@ -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,
);
},
);
});
}

View File

@ -25,6 +25,7 @@ void main() {
name: fieldName,
layout: ViewLayoutPB.Board,
);
await tester.dismissRowDetailPage();
await tester.tapButton(card1);
await tester.changeFieldTypeOfFieldWithName(
fieldName,

View File

@ -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);
});
});
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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();
});

View File

@ -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()]);
});
});
}

View File

@ -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);
});
});
}

View File

@ -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();
});
});
}

View File

@ -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));

View File

@ -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');

View File

@ -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);
});
});
});
}

View File

@ -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));
});
});
}

View File

@ -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));
});
});
}

View File

@ -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));
});
});
}

View File

@ -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),
];
}
}

View File

@ -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';

View File

@ -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.
}

View File

@ -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();
}

View File

@ -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);

View File

@ -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';
}

View File

@ -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));
}

View File

@ -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);
}
}

View File

@ -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';

View File

@ -56,6 +56,8 @@ PODS:
- Flutter
- keyboard_height_plugin (0.0.1):
- Flutter
- open_file_ios (0.0.1):
- Flutter
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5):
@ -102,6 +104,7 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@ -148,6 +151,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/irondash_engine_context/ios"
keyboard_height_plugin:
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
@ -170,7 +175,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
@ -180,25 +185,26 @@ SPEC CHECKSUMS:
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_file_ios: 461db5853723763573e140de3193656f91990d9e
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca

View File

@ -35,6 +35,7 @@ class KVKeys {
'kDocumentAppearanceCursorColor';
static const String kDocumentAppearanceSelectionColor =
'kDocumentAppearanceSelectionColor';
static const String kDocumentAppearanceWidth = 'kDocumentAppearanceWidth';
/// The key for saving the expanded views
///

View File

@ -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,

View File

@ -90,7 +90,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
),
onPressed: () {
final name = state.content;
_openRowPage(context, name);
_openRowPage(context, name ?? "");
},
),
);

View File

@ -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,
),
),
],
),
),
],
);
}
}

View File

@ -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,
),
),

View File

@ -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;

View File

@ -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;

View File

@ -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:

View File

@ -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

View File

@ -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>;

View File

@ -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,

View File

@ -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

View File

@ -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,
),
);
}

View File

@ -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),

View File

@ -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),

View File

@ -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,
),
),
],
),

View File

@ -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();
},

View File

@ -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();

View File

@ -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(

View File

@ -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));
},
);
}

View File

@ -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,
),
);
},
),
),
);
},
),

View File

@ -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);
},

View File

@ -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

View File

@ -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,
);
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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),
],
),
),
);

View File

@ -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,
),
);
}
}

View File

@ -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});

View File

@ -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,
),
);

View File

@ -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,
),
),
],
),
);
},
),
);
}
}

View File

@ -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,

View File

@ -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,
),
);
}
}

View File

@ -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();
},
);
},
);
},
);

View File

@ -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),

View File

@ -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) {

View File

@ -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(

View File

@ -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,
),
);

View File

@ -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,
),
),
),

View File

@ -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,
),
),
);
}

View File

@ -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));
},
),
);

View File

@ -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(

View File

@ -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(),
),
),
),

View File

@ -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: [

View File

@ -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()

View File

@ -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));
}
}
}

View File

@ -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),

View File

@ -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(

View File

@ -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();

View File

@ -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,
);
}
}

View File

@ -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),
],
);
}

View File

@ -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');

View File

@ -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,
);
}
}

View File

@ -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;
}

View File

@ -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,
);
}
}

View File

@ -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;
}

View File

@ -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;
}
},

View File

@ -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));

View File

@ -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();

View File

@ -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;

View File

@ -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);

View File

@ -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();

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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