Compare commits

...
Author SHA1 Message Date
nathan c404ce5297 chore: clippy 2024-10-15 17:12:07 +08:00
nathan 64dfad284e chore: use same runtime 2024-10-06 22:55:50 +08:00
nathan ea34b226f1 chore: update calculation when filter change 2024-10-06 21:46:15 +08:00
nathan 3b696e4d27 Merge branch 'main' into fix_calculation_bug 2024-10-06 20:14:29 +08:00
Nathan.fooo fd9b01ca27 fix: import data (#6483)
* chore: import into space

* chore: par insert

* fix: migrate from anon

* chore: fix test

* chore: update test

* chore: add test

* chore: update test

* chore: update test

* chore: update docs

* fix: space collab

* chore: update test
2024-10-06 20:08:17 +08:00
Lucas 1a82f3fff1 fix: title flash when resizing (#6481) 2024-10-05 21:11:45 +08:00
Richard Shiue 8e6f051dec fix: launch review issues (#6479)
* fix(flutter_desktop): field icon picker popover constraints

* fix(mobile): immediately select field when editing filter

* fix: filter through select options not working

* fix(mobile): scroll to bottom after filter creation

* fix: primary field tooltip
2024-10-05 10:57:52 +08:00
Lucas 8ceff03f3e feat: add more bottom padding in workspace menu (#6469)
* chore: add more bottom padding in create workspace menu on mobile

* feat: add more bottom padding in workspace menu

* feat: enable scrollable in workspace menu on desktop

* feat: enable to customize filiter rules

* fix: workspace menu icon dark mode issue

* Revert "feat: enable to customize filiter rules"

This reverts commit 3e6d522112.
2024-10-04 15:50:36 +08:00
Lucas e1bbcc415f chore: disable copy link to block feature (#6472) 2024-10-04 14:57:46 +08:00
Kilu.He 567ba7d962 fix: not found page issues (#6471) 2024-10-04 14:00:51 +08:00
Lucas 153416604d fix: ingore keyup event in cover title (#6468)
* fix: ingore keyup event in cover title

* feat: add text field with line metric

* chore: refactor test strcuture

* test: add arrow down key test
2024-10-04 14:00:10 +08:00
Kilu.He caa882dc37 fix: add some placeholder for deleted and not found (#6470) 2024-10-04 13:52:01 +08:00
Kilu.He 9c05788834 fix: share link issues (#6465) 2024-10-04 12:51:17 +08:00
Mathias Mogensen 6b585ef9a7 fix: do not bring back purposeful null selection (#6464)
* fix: do not bring back purposeful null selection

* test: add more tests

* fix: redundant import
2024-10-03 23:05:30 +02:00
Nathan.fooo 469ddacfcc chore: bump collab (#6450)
* chore: upgrade collab

* chore: bump client api
2024-10-03 23:45:29 +08:00
nathan e714548dc3 chore: cal when open database 2024-10-03 22:51:40 +08:00
Kilu.He 4d4e3bfb8a fix: add backspace behavior test cases (#6463) 2024-10-03 22:37:40 +08:00
nathan 1ba339fabe chore: optimize calculate 2024-10-03 16:17:27 +08:00
Richard Shiue 257a7530a0 chore: bump version 0.7.1 (#6460) 2024-10-03 14:41:31 +08:00
Lucas 02126a1e24 chore: update client api (#6459) 2024-10-03 14:31:19 +08:00
Lucas 97913c390b feat: support share page and block (#6431)
* feat: support share page

* feat: support copy share link

* chore: replace share icon

* chore: update translations

* chore: optimize code

* test: add share link test

* feat: support copy block link

* test: add copy link to block test

* chore: refactor share code

* fix: doc bloc not found issue
2024-10-03 14:31:04 +08:00
Kilu.He c1cf58b99e feat: support withYHistory and editing document on web (#6438)
* feat: support withYHistory

* fix: inline elements

* fix: insert break in single line

* fix: add enter key behavior tests

* fix: add test cases

* fix: add test cases
2024-10-03 13:54:42 +08:00
Lucas 574b0ce84b fix: windows CI failed (#6457)
* chore: update flutter & rust version

* chore: try to fix windows ci

* chore: downgrade windows version to 2019
2024-10-03 13:28:02 +08:00
nathan 90cf10e2c4 fix: calculate value 2024-10-03 13:27:35 +08:00
Mathias Mogensen 26ac896bac test: amend tests after automatic merge (#6458) 2024-10-03 05:48:32 +02:00
Mathias Mogensen b36babf754 feat: resume selection on window focus (#6455) 2024-10-03 05:44:44 +02:00
Mathias Mogensen 0d69b895aa feat: sub page block (#6427)
* feat: base subpage block and behavior

* fix: do not record undo

* refactor: add BlockTransactionHandler

* test: start adding coverage

* test: delete w/ backspace

* test: add to runner

* fix: rebuild issue on create

* test: copy+paste base test

* fix: conflict behavior and test coverage

* fix: after merge

* test: add wait duration

* test: more tests

* fix: cut behavior + tests

* fix: refactor copy+paste and more test cov

* fix: localization + test coverage + cleanup

* test: add drag subpageblock node test

* test: remove backspace test
2024-10-03 04:58:08 +02:00
Mathias Mogensen 3b48ca0f4b fix: new dialog style in trash actions (#6456)
* fix: new dialog style in trash actions

* fix: restore dialog
2024-10-03 04:38:50 +02:00
ja49619 2e6ce16b11 chore: update ru-RU translations 🐦 (#6384) 2024-10-03 10:19:02 +08:00
Richard Shiue edeec49121 fix: hide editing rows from groups when filters are applied (#6409)
* fix: editing a row while in active filters

* fix: dont allow filtering by group field
2024-10-03 10:17:51 +08:00
Lucas 5de716b19d feat: update workspace icon style on desktop (#6452)
* feat: update workspace icon style on desktop

* fix: unable to insert two referenced link at the same doc
2024-10-03 09:39:52 +08:00
Mathias Mogensen cb2b933a90 feat: show file popover automatically (#6453) 2024-10-03 03:25:09 +02:00
Mathias Mogensen 37f99988eb fix: hide border on icon in callout (#6454) 2024-10-03 03:12:33 +02:00
Richard Shiue 99962dc3c6 fix: field type option transform (#6451)
* test: add field transform tests

* fix: type option transform messing up type option data

* test: add date time transform integration test

* fix: update view editor
2024-10-03 08:11:11 +08:00
nathan bec1668272 chore: upgrade collab 2024-10-02 22:21:04 +08:00
DongHyeon 063d54dfae chore(i18n): update ko-KR translations (#6378)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-10-02 20:54:22 +08:00
Sean SidersandLucas.Xu 59f79be290 feat(flutter_desktop): clickable url links in row detail (#5916)
* feat: Url row detail openable via `ctrl | meta + click`

* feat: Optimize `LinkTextField` state management

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-10-02 20:52:52 +08:00
Lucas 813c8e6b86 feat: support workspace ops on mobile (#6449)
* feat: support workspace ops on mobile

* chore: move the member bloc to workspace menu item widget

* feat: support creating workspace on mobile

* chore: add popToHome extension

* fix: flutter analyze

* feat: support renaming a workspace

* feat: support deleting a workspace

* feat: support leaving a workspace

* feat: workspace icon ui revamp

* feat: support updating workspace icon on mobile

* feat: show a confirm dialog before deleting a workspace

* fix: workspace name overflow

* feat: support leaving a workspace

* chore: update translations

* feat: show a toast after renaming workspace

* feat: update translations

* feat: add workspace operation integration tests on mobile

* test: add create workspace test on mobile
2024-10-02 20:13:19 +08:00
Richard Shiue 23e3650570 test(flutter_desktop): add edit row test (#6442) 2024-10-02 09:17:28 +08:00
Richard Shiue 092aefdd71 fix(flutter_desktop): fix calendar event bugs (#6446)
* fix: row controller being disposed too early in calendar

* fix: empty accessories in calendar event card
2024-10-02 09:17:02 +08:00
Lucas 19361e2c68 feat: optimize selection gesture in editor (#6445) 2024-10-01 20:14:53 +08:00
Lucas 6e48f02d25 fix: insert the pasted nodes after current selection if they start with a non-delta node (#6441)
* fix: pasting in-app json on a line with text can cause corrupt blocks

* chore: update editor

* fix: paste multiple nodes test
2024-10-01 17:58:10 +08:00
Lucas 46e45c3715 feat: keep link format when converting preview block to text (#6435)
* feat: keep link format when converting preview block to text

* test: add test

* fix: flutter analyze

* feat: ctrl/cmd+z to revert the link prevew op

* test: add test

* chore: update toast style
2024-10-01 12:53:44 +08:00
Lucas eca495ce63 fix: double cursor show in title and doc sometimes (#6437) 2024-10-01 11:57:51 +08:00
Richard Shiue 70e72d843e feat: custom database field icons (#6415) 2024-09-30 22:14:15 +08:00
Richard Shiue 7ac3b72a4f feat(flutter_desktop): use regular rather than medium font weight by default (#6432)
* feat(flutter_desktop): use regular rather than medium by default

* fix: setting button regression

* fix: bring more text styles up to date with design
2024-09-30 21:37:11 +08:00
Mathias Mogensen cc9072e9cc chore: remove supabase dependency (#6396) 2024-09-30 12:17:23 +02:00
Lucas a0ee47b809 feat: support copy/cut command when the selection is collapsed (#6429)
* feat: add delete line command

* test: add delete line test

* test: add delete line command test

* feat: support copy command when selection is collasped
2024-09-30 16:34:41 +08:00
Richard Shiue af40ff8eb1 chore: grid ui improvements (#6428)
* fix: dont use medium font weight

* fix: widget padding adjustments

* fix: new divider color

* chore: use horizontal padding from databsepluginwidgetbuildersize

* chore: use existing color token

* chore: restore old add icon
2024-09-30 16:09:57 +08:00
Richard Shiue b0211be03c fix(flutter_desktop): field info not updating in sort info (#6430) 2024-09-30 16:08:49 +08:00
Kilu.He 3174ffed5c fix: support approve (#6413) 2024-09-30 13:41:47 +08:00
Richard Shiue 99c7252a15 fix(flutter_desktop): board card rebuild issue (#6424) 2024-09-29 11:34:45 +08:00
Lucas 99c81fed56 fix: paste doesn't work sometimes in the first try (#6412)
* fix: paste doesn't work sometimes in the first try

* test: paste the url that doesn't contain a protocol

* feat: skip parsing as a preview block if the url is image
2024-09-27 17:45:49 +08:00
Lucas a01fc5b572 feat: support window tiling on macos (#6410) 2024-09-27 15:22:18 +08:00
Lucas 1b0bb1d5ff fix: backtick issue in French IME (#6408)
* fix: backtick doesn't format the text to code in french ime

* test: add backtick test

* fix: three backticks doesn't convert to code block in french ime

* chore: remove cache for docker ci
2024-09-26 20:16:49 +08:00
Lucas 1ffd653515 fix: grid cell date time test (#6406)
* fix: grid cell date time test

* chore: delete test
2024-09-26 20:16:33 +08:00
Richard Shiue e1c60084d3 feat: allow filtering by last modified and created at field types (#6407)
* feat: last modified and created on filters

* chore: improve select option filter

* test: add test
2024-09-26 18:57:15 +08:00
LucasandMathias Mogensen 4c6b6ffdac feat: support cover title (#6353)
* feat: support cover title

* feat: support arrow down and arrow right key on cover title

* feat: support arrow up and arrow left key on editor

* test: add integration test

* chore: update frontend/appflowy_flutter/integration_test/desktop/document/document_title_test.dart

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

* feat: use shared document context to save title focus node

* fix: the backspace key doesn't work after pressing Enter in the title

* feat: support pressing arrow left key to focus on title

* fix: integration test

* test: edit title and insert new line

* test: arrow key in title test

* test: check if the title is saved

* fix: flutter analyze

* test: add cover title command tests

* fix: integration tests

* test: change the title via sidebar, check the title is updated

* test: set default name when pasting content

* fix: field visibility test

* fix: 'create a new workspace, open it and then delete it' test

* fix: create a new document and move it to Getting started test

* test: fix integration tests

* fix: 'create a new document and edit title' failed on linux

* fix: shortcut and create a new page test

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2024-09-25 22:44:59 +08:00
Richard Shiue 783d458100 feat(mobile): database filters (#6401) 2024-09-25 22:22:36 +08:00
Richard Shiue 9b7d38bca7 feat(flutter_desktop): allow filtering by date end (#6399) 2024-09-25 15:28:47 +08:00
Lucas a8a8502577 chore: install deps using pacman (#6397)
* chore: install deps using pacman

* chore: use rust 1.80.1

* chore: add docker ci

* fix: docker ci

* chore: optimize docker-ci speed
2024-09-25 15:25:44 +08:00
Nathan.fooo 6a1619d300 chore: detect field type when import csv (#6400) 2024-09-25 15:09:00 +08:00
Richard Shiue 9b811f0166 refactor(flutter_desktop): filter blocs (#6356)
* refactor: database filter bloc

* refactor: filter choice chips

* fix: update field info in filters

* refactor: simplify select option filter panel

* test: fix tests

* refactor: filter choice chips

* test: add bloc tests

* refactor: date time filter bloc and choice chip
2024-09-25 12:04:00 +08:00
Nathan.fooo 7e1b77f872 chore: Migrate type option (#6395)
* chore: migrate number type option

* chore: migrate single select

* chore: migrate multi select

* chore: migrate date type option

* chore: migrate timestamp type option

* chore: support friendly full

* chore: migrate media type option

* chore: update collab

* chore: update zip

* chore: remove file

* chore: fix build

* chore: fix test
2024-09-25 11:44:19 +08:00
Mohammad Zolfaghari b7c598ea56 feat(flutter_desktop): date filter (#6288)
* feat: wip enabling date filter

* fix: update date picker selected range on change

* fix: save in utc, load in local date time

* test: added date filter test

* fix: don't include time in date picker

including time makes filtering complex without much gain.
when the condition is "is" we should also note the exact time
which most of the cases the time is not intended by user only the day.

* fix: flutter analyze
2024-09-24 22:03:07 +08:00
Richard Shiue da7c993fd6 chore(flutter_desktop): enable selecting icons without backgrounds (#6379)
* chore: enable selecting icons without backgrounds

* chore: remove debugprint

* chore: rename flag
2024-09-24 22:01:21 +08:00
Richard Shiue c6dd5db6da fix: edge case for text filters (#6387) 2024-09-24 22:01:05 +08:00
Mathias Mogensen e125e9493a fix: offset row actions by cover height (#6390) 2024-09-24 14:33:23 +02:00
Lucas 5fbd07e96a fix: potential ai_repository not init (#6388)
* fix: potential ai_repository not init

* fix: assertion when deleting block

* chore: remove the rule

* test: add smart edit bloc test

* test: add delete block test

* chore: remove unused_import

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

* feat: set image from media cell as cover

* fix: duplicate row meta when duplicating row

* fix: use serialize repr and deserialize repr

* chore: update collab revision

* fix: failing test and bug w/ document icon

* fix: show empty cover on load failure

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

* chore: code style

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

* test: add cloud server test

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

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

* fix: zoom in test assertion

* fix: zoom in/out test

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

* feat: disable moving the page to a database

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

* fix: document width doesn't refresh after resetting

* fix: disable tooltip when dragging blocks

* fix: reminder not render when dragging

* chore: optimize code

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

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

* fix: tooltip assertion

* fix: disable table cell block

* chore: optimize moving logic

* test: collaborative workspace test

* test: move to page test

* test: drag block test

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

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

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

* fix: tooltip showing when hovering over empty space

* fix: text not centered

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

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

* feat: open document and database

* feat: support editing

* fix: lint

* fix: add share actions

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

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

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

* feat: add settings entry in sign in page

* feat: support exporting log files on desktop

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

* fix: unable to switch to another workspace

* fix: integration test

* chore: update editor version

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

* chore: refactor padding calculation function

* fix: remove reset confim dialog

* chore: remove unused code

* feat: update sider position

* feat: use max width instead of padding

* fix: remove unused code

* fix: padding is negative error

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

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

* feat: convert backtick to codeblock

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

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

---------

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

* fix: tests

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

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

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

* test: add grid integration tests

* test: fix delete group test

* chore: fix rust-lib tests

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

* feat: render feedback widget

* feat: add drag to move translation

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

* feat: render table placeholder

* feat: implement auto scroll when dragging to edge

* chore: add back the drop images/files feature

* chore: code refactor

* feat: exclude image and file block

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

* chore: remove cancellation when get rows

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

* chore: use on complete edit callback

* chore: fix test

* chore: fix test

---------

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

* chore: clean debugPrint

* chore: update collab rev

* feat: add cover functionality

* fix: provider issue for calendarbloc in popover

* feat: mobile row cover

* fix: imports after merge

* test: add media test to runner

* fix: mobile cover

* test: add row cover tests

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

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

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

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

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

* chore: write database rows

* chore: fix test

* chore: fix test

* chore: bump collab

* chore: fix get related row

* chore: try to fix open database error

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

* chore: copywriting fixes

* fix: mobile cell

* feat: browse all images in a cell in viewer

* chore: unused variable

* fix: dialogs on download file

* feat: upload multiple files at once

* test: improve test coverage

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

* chore: blocking read

* chore: fix test

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

added links to App Store and Play Store

* Update README.md

---------

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

* chore: update appflowy_editor

* chore: enable editor log when enableDocumentInternalLog is true

* chore: update podfile.lock

* Revert "chore: update podfile.lock"

This reverts commit 41dbcad8c1.

* fix: compile

* chore: revert hotkey changes

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

* fix: rename group event

* chore: add comment for rough design

* chore: revert unintentional changes

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

* chore: update client api

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

* feat: use flutter_bloc to manage ai response

* feat: integrate smart_edit_bloc

* fix: bloc not found

* feat: support 'fix spelling and grammer'

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

* feat: support moving cursor using keyboard gesture on mobile

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

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

No files matched your search

+18 -13
View File
@@ -58,19 +58,24 @@ runs:
- name: Install prerequisites
working-directory: frontend
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv
elif [ "$RUNNER_OS" == "Windows" ]; then
vcpkg integrate install
elif [ "$RUNNER_OS" == "macOS" ]; then
echo 'do nothing'
fi
cargo make appflowy-flutter-deps-tools
shell: bash
run: |
case $RUNNER_OS in
Linux)
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
;;
Windows)
vcpkg integrate install
vcpkg update
;;
macOS)
# No additional prerequisites needed for macOS
;;
esac
cargo make appflowy-flutter-deps-tools
- name: Build AppFlowy
working-directory: frontend
@@ -94,4 +99,4 @@ runs:
- uses: actions/upload-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
path: appflowy_flutter.tar.gz
path: appflowy_flutter.tar.gz
+27 -31
View File
@@ -2,18 +2,10 @@ name: Docker-CI
on:
push:
branches:
- main
- release/*
paths:
- frontend/**
branches: ["main", "release/*"]
pull_request:
branches:
- main
- release/*
paths:
- frontend/**
types: [ opened, synchronize, reopened, unlocked, ready_for_review ]
branches: ["main", "release/*"]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -27,25 +19,29 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Docker Compose
run: |
docker-compose --version || {
echo "Docker Compose not found, installing..."
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# cache the docker layers
# don't cache anything temporarly, because it always triggers "no space left on device" error
# - name: Cache Docker layers
# uses: actions/cache@v3
# with:
# path: /tmp/.buildx-cache
# key: ${{ runner.os }}-buildx-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-buildx-
- name: Build the app
shell: bash
run: |
set -eu -o pipefail
cd frontend/scripts/docker-buildfiles
docker-compose build --no-cache --progress=plain \
| while read line; do \
if [[ "$line" =~ ^Step[[:space:]] ]]; then \
echo "$(date -u '+%H:%M:%S') | $line"; \
else \
echo "$line"; \
fi; \
done
uses: docker/build-push-action@v5
with:
context: .
file: ./frontend/scripts/docker-buildfiles/Dockerfile
push: false
# cache-from: type=local,src=/tmp/.buildx-cache
# cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# - name: Move cache
# run: |
# rm -rf /tmp/.buildx-cache
# mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+14 -11
View File
@@ -25,9 +25,9 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.77.2"
CARGO_MAKE_VERSION: "0.36.6"
FLUTTER_VERSION: "3.22.2"
RUST_TOOLCHAIN: "1.80.1"
CARGO_MAKE_VERSION: "0.37.18"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -73,9 +73,9 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ windows-latest ]
os: [ windows-2019 ]
include:
- os: windows-latest
- os: windows-2019
flutter_profile: development-windows-x86
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
@@ -241,12 +241,15 @@ jobs:
cp deploy.env .env
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env
sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
- name: Run Docker-Compose
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
APPFLOWY_CLOUD_VERSION: 0.6.29-amd64
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
@@ -317,7 +320,7 @@ jobs:
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
docker ps -a
flutter test integration_test/cloud/cloud_runner.dart -d Linux --coverage
flutter test integration_test/desktop/cloud/cloud_runner.dart -d Linux --coverage
shell: bash
# split the integration tests into different machines to minimize the time
@@ -330,7 +333,7 @@ jobs:
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
@@ -360,7 +363,7 @@ jobs:
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
@@ -390,7 +393,7 @@ jobs:
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
@@ -409,4 +412,4 @@ jobs:
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
rust_target: ${{ matrix.target }}
+10 -10
View File
@@ -232,10 +232,10 @@ jobs:
matrix:
job:
- {
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-latest,
extra-build-args: "",
}
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-latest,
extra-build-args: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -336,12 +336,12 @@ jobs:
matrix:
job:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
+5
View File
@@ -47,6 +47,8 @@ jobs:
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
@@ -127,12 +129,15 @@ jobs:
run: |
cp deploy.env .env
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
- name: Ensure AppFlowy-Cloud is Running with Correct Version
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
+26
View File
@@ -1,4 +1,30 @@
# Release Notes
## Version 0.7.0 - 19/09/2024
### New Features
- Support reordering blocks in document with drag and drop
- Support for adding a cover to a row/card in databases
- Added support for accessing settings on the sign-in page
- Added "Move to" option to the document menu in top right corner
- Support for adjusting the document width from settings
- Show full name of a group on hover
- Colored group names in kanban boards
- Support "Ask AI" on multiple lines of text
- Support for keyboard gestures to move cursor on Mobile
- Added markdown support for quickly inserting a code block using three backticks
### Bug Fixes
- Fixed a critical bug where the backtick character would crash the application
- Fixed an issue with signing-in from the settings dialog where the dialog would persist
- Fixed a visual bug with icon alignment in primary cell of database rows
- Fixed a bug with filters applied where new rows were inserted in wrong position
- Fixed a bug where "Untitled" would override the name of the row
- Fixed page title not updating after renaming from "More"-menu
- Fixed File block breaking row detail document
- Fixed issues with reordering rows with sorting rules applied
- Improvements to the File & Media type in Database
- Performance improvement in Grid view
- Fixed filters sometimes not applying properly in databases
## Version 0.6.9 - 09/09/2024
### New Features
- Added a new property type, 'Files & media'
+2 -2
View File
@@ -44,8 +44,8 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases)
- Other channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
- Available on
- App Store: iPhone
- Play Store: Android 10 or above; ARMv7 is not supported
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is not supported
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.9"
APPFLOWY_VERSION = "0.7.1"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -0,0 +1,14 @@
"{""id"":""RGmzka"",""name"":""Name"",""field_type"":0,""type_options"":{""0"":{""data"":""""}},""is_primary"":true}","{""id"":""oYoH-q"",""name"":""Time Slot"",""field_type"":2,""type_options"":{""0"":{""date_format"":3,""data"":"""",""time_format"":1,""timezone_id"":""""},""2"":{""date_format"":3,""time_format"":1,""timezone_id"":""""}},""is_primary"":false}","{""id"":""zVrp17"",""name"":""Amount"",""field_type"":1,""type_options"":{""1"":{""scale"":0,""format"":4,""name"":""Number"",""symbol"":""RUB""},""0"":{""data"":"""",""symbol"":""RUB"",""name"":""Number"",""format"":0,""scale"":0}},""is_primary"":false}","{""id"":""_p4EGt"",""name"":""Delta"",""field_type"":1,""type_options"":{""1"":{""name"":""Number"",""format"":36,""symbol"":""RUB"",""scale"":0},""0"":{""data"":"""",""symbol"":""RUB"",""name"":""Number"",""format"":0,""scale"":0}},""is_primary"":false}","{""id"":""Z909lc"",""name"":""Email"",""field_type"":6,""type_options"":{""6"":{""url"":"""",""content"":""""},""0"":{""data"":"""",""content"":"""",""url"":""""}},""is_primary"":false}","{""id"":""dBrSc7"",""name"":""Registration Complete"",""field_type"":5,""type_options"":{""5"":{}},""is_primary"":false}","{""id"":""VoigvK"",""name"":""Progress"",""field_type"":7,""type_options"":{""0"":{""data"":""""},""7"":{}},""is_primary"":false}","{""id"":""gbbQwh"",""name"":""Attachments"",""field_type"":14,""type_options"":{""0"":{""data"":"""",""content"":""{\""files\"":[]}""},""14"":{""content"":""{\""files\"":[]}""}},""is_primary"":false}","{""id"":""id3L0G"",""name"":""Priority"",""field_type"":3,""type_options"":{""3"":{""content"":""{\""options\"":[{\""id\"":\""cplL\"",\""name\"":\""VIP\"",\""color\"":\""Purple\""},{\""id\"":\""GSf_\"",\""name\"":\""High\"",\""color\"":\""Blue\""},{\""id\"":\""qnja\"",\""name\"":\""Medium\"",\""color\"":\""Green\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""541SFC"",""name"":""Tags"",""field_type"":4,""type_options"":{""0"":{""data"":"""",""content"":""{\""options\"":[],\""disable_color\"":false}""},""4"":{""content"":""{\""options\"":[{\""id\"":\""1i4f\"",\""name\"":\""Education\"",\""color\"":\""Yellow\""},{\""id\"":\""yORP\"",\""name\"":\""Health\"",\""color\"":\""Orange\""},{\""id\"":\""SEUo\"",\""name\"":\""Hobby\"",\""color\"":\""LightPink\""},{\""id\"":\""uRAO\"",\""name\"":\""Family\"",\""color\"":\""Pink\""},{\""id\"":\""R9I7\"",\""name\"":\""Work\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""lg0B7O"",""name"":""Last modified"",""field_type"":8,""type_options"":{""0"":{""time_format"":1,""field_type"":8,""date_format"":3,""data"":"""",""include_time"":true},""8"":{""date_format"":3,""field_type"":8,""time_format"":1,""include_time"":true}},""is_primary"":false}","{""id"":""5riGR7"",""name"":""Created at"",""field_type"":9,""type_options"":{""0"":{""field_type"":9,""include_time"":true,""date_format"":3,""time_format"":1,""data"":""""},""9"":{""include_time"":true,""field_type"":9,""date_format"":3,""time_format"":1}},""is_primary"":false}"
"{""data"":""Olaf"",""created_at"":1726063289,""last_modified"":1726063289,""field_type"":0}","{""last_modified"":1726122374,""created_at"":1726110045,""reminder_id"":"""",""is_range"":true,""include_time"":true,""end_timestamp"":""1725415200"",""field_type"":2,""data"":""1725256800""}","{""field_type"":1,""data"":""55200"",""last_modified"":1726063592,""created_at"":1726063592}","{""last_modified"":1726062441,""created_at"":1726062441,""data"":""0.5"",""field_type"":1}","{""created_at"":1726063719,""last_modified"":1726063732,""data"":""doyouwannabuildasnowman@arendelle.gov"",""field_type"":6}",,"{""field_type"":7,""last_modified"":1726064207,""data"":""{\""options\"":[{\""id\"":\""oqXQ\"",\""name\"":\""find elsa\"",\""color\"":\""Purple\""},{\""id\"":\""eQwp\"",\""name\"":\""find anna\"",\""color\"":\""Purple\""},{\""id\"":\""5-B3\"",\""name\"":\""play in the summertime\"",\""color\"":\""Purple\""},{\""id\"":\""UBFn\"",\""name\"":\""get a personal flurry\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""oqXQ\"",\""eQwp\"",\""UBFn\""]}"",""created_at"":1726064129}",,"{""created_at"":1726065208,""data"":""cplL"",""last_modified"":1726065282,""field_type"":3}","{""field_type"":4,""data"":""1i4f"",""last_modified"":1726105102,""created_at"":1726105102}","{""field_type"":8,""data"":""1726122374""}","{""data"":""1726060476"",""field_type"":9}"
"{""field_type"":0,""last_modified"":1726063323,""data"":""Beatrice"",""created_at"":1726063323}",,"{""last_modified"":1726063638,""data"":""828600"",""created_at"":1726063607,""field_type"":1}","{""field_type"":1,""created_at"":1726062488,""data"":""-2.25"",""last_modified"":1726062488}","{""last_modified"":1726063790,""data"":""btreee17@gmail.com"",""field_type"":6,""created_at"":1726063790}","{""created_at"":1726062718,""data"":""Yes"",""field_type"":5,""last_modified"":1726062724}","{""created_at"":1726064277,""data"":""{\""options\"":[{\""id\"":\""BDuH\"",\""name\"":\""get the leaf node\"",\""color\"":\""Purple\""},{\""id\"":\""GXAr\"",\""name\"":\""upgrade to b+\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""field_type"":7,""last_modified"":1726064293}",,"{""data"":""GSf_"",""created_at"":1726065288,""last_modified"":1726065288,""field_type"":3}","{""created_at"":1726105110,""data"":""yORP,uRAO"",""last_modified"":1726105111,""field_type"":4}","{""data"":""1726105111"",""field_type"":8}","{""field_type"":9,""data"":""1726060476""}"
"{""last_modified"":1726063355,""created_at"":1726063355,""field_type"":0,""data"":""Lancelot""}","{""data"":""1726468159"",""is_range"":true,""end_timestamp"":""1726727359"",""reminder_id"":"""",""include_time"":false,""field_type"":2,""created_at"":1726122403,""last_modified"":1726122559}","{""created_at"":1726063617,""last_modified"":1726063617,""data"":""22500"",""field_type"":1}","{""data"":""11.6"",""last_modified"":1726062504,""field_type"":1,""created_at"":1726062504}","{""field_type"":6,""data"":""sir.lancelot@gmail.com"",""last_modified"":1726063812,""created_at"":1726063812}","{""data"":""No"",""field_type"":5,""last_modified"":1726062724,""created_at"":1726062375}",,,"{""data"":""cplL"",""created_at"":1726065286,""last_modified"":1726065286,""field_type"":3}","{""last_modified"":1726105237,""data"":""SEUo"",""created_at"":1726105237,""field_type"":4}","{""field_type"":8,""data"":""1726122559""}","{""field_type"":9,""data"":""1726060476""}"
"{""data"":""Scotty"",""last_modified"":1726063399,""created_at"":1726063399,""field_type"":0}","{""reminder_id"":"""",""last_modified"":1726122418,""include_time"":true,""data"":""1725868800"",""end_timestamp"":""1726646400"",""created_at"":1726122381,""field_type"":2,""is_range"":true}","{""created_at"":1726063650,""last_modified"":1726063650,""data"":""10900"",""field_type"":1}","{""data"":""0"",""created_at"":1726062581,""last_modified"":1726062581,""field_type"":1}","{""last_modified"":1726063835,""created_at"":1726063835,""field_type"":6,""data"":""scottylikestosing@outlook.com""}","{""data"":""Yes"",""field_type"":5,""created_at"":1726062718,""last_modified"":1726062718}","{""created_at"":1726064309,""data"":""{\""options\"":[{\""id\"":\""Cw0K\"",\""name\"":\""vocal warmup\"",\""color\"":\""Purple\""},{\""id\"":\""nYMo\"",\""name\"":\""mixed voice training\"",\""color\"":\""Purple\""},{\""id\"":\""i-OX\"",\""name\"":\""belting training\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""Cw0K\"",\""nYMo\"",\""i-OX\""]}"",""field_type"":7,""last_modified"":1726064325}","{""last_modified"":1726122911,""created_at"":1726122835,""data"":[""{\""id\"":\""746a741d-98f8-4cc6-b807-a82d2e78c221\"",\""name\"":\""googlelogo_color_272x92dp.png\"",\""url\"":\""https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}"",""{\""id\"":\""cbbab3ee-32ab-4438-a909-3f69f935a8bd\"",\""name\"":\""tL_v571NdZ0.svg\"",\""url\"":\""https://static.xx.fbcdn.net/rsrc.php/y9/r/tL_v571NdZ0.svg\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Link\""}""],""field_type"":14}",,"{""data"":""SEUo,yORP"",""field_type"":4,""last_modified"":1726105123,""created_at"":1726105115}","{""data"":""1726122911"",""field_type"":8}","{""data"":""1726060539"",""field_type"":9}"
"{""field_type"":0,""created_at"":1726063405,""last_modified"":1726063421,""data"":""""}",,,"{""last_modified"":1726062625,""field_type"":1,""data"":"""",""created_at"":1726062607}",,"{""data"":""No"",""last_modified"":1726062702,""created_at"":1726062393,""field_type"":5}",,,,,"{""data"":""1726063421"",""field_type"":8}","{""data"":""1726060539"",""field_type"":9}"
"{""field_type"":0,""data"":""Thomas"",""last_modified"":1726063421,""created_at"":1726063421}","{""reminder_id"":"""",""field_type"":2,""data"":""1725627600"",""is_range"":false,""created_at"":1726122583,""last_modified"":1726122593,""end_timestamp"":"""",""include_time"":true}","{""last_modified"":1726063666,""field_type"":1,""data"":""465800"",""created_at"":1726063666}","{""last_modified"":1726062516,""field_type"":1,""created_at"":1726062516,""data"":""-0.03""}","{""field_type"":6,""last_modified"":1726063848,""created_at"":1726063848,""data"":""tfp3827@gmail.com""}","{""field_type"":5,""last_modified"":1726062725,""data"":""Yes"",""created_at"":1726062376}","{""created_at"":1726064344,""data"":""{\""options\"":[{\""id\"":\""D6X8\"",\""name\"":\""brainstorm\"",\""color\"":\""Purple\""},{\""id\"":\""XVN9\"",\""name\"":\""schedule\"",\""color\"":\""Purple\""},{\""id\"":\""nJx8\"",\""name\"":\""shoot\"",\""color\"":\""Purple\""},{\""id\"":\""7Mrm\"",\""name\"":\""edit\"",\""color\"":\""Purple\""},{\""id\"":\""o6vg\"",\""name\"":\""publish\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""D6X8\""]}"",""last_modified"":1726064379,""field_type"":7}",,"{""last_modified"":1726065298,""created_at"":1726065298,""field_type"":3,""data"":""GSf_""}","{""data"":""yORP,SEUo"",""field_type"":4,""last_modified"":1726105229,""created_at"":1726105229}","{""data"":""1726122593"",""field_type"":8}","{""field_type"":9,""data"":""1726060540""}"
"{""data"":""Juan"",""last_modified"":1726063423,""created_at"":1726063423,""field_type"":0}","{""created_at"":1726122510,""reminder_id"":"""",""include_time"":false,""is_range"":true,""last_modified"":1726122515,""data"":""1725604115"",""end_timestamp"":""1725776915"",""field_type"":2}","{""field_type"":1,""created_at"":1726063677,""last_modified"":1726063677,""data"":""93100""}","{""field_type"":1,""data"":""4.86"",""created_at"":1726062597,""last_modified"":1726062597}",,"{""last_modified"":1726062377,""field_type"":5,""data"":""Yes"",""created_at"":1726062377}","{""last_modified"":1726064412,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""tTDq\"",\""name\"":\""complete onboarding\"",\""color\"":\""Purple\""},{\""id\"":\""E8Ds\"",\""name\"":\""contact support\"",\""color\"":\""Purple\""},{\""id\"":\""RoGN\"",\""name\"":\""get started\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""tTDq\"",\""E8Ds\""]}"",""created_at"":1726064396}",,"{""created_at"":1726065278,""field_type"":3,""data"":""qnja"",""last_modified"":1726065278}","{""data"":""R9I7,yORP,1i4f"",""field_type"":4,""created_at"":1726105126,""last_modified"":1726105127}","{""data"":""1726122515"",""field_type"":8}","{""data"":""1726060541"",""field_type"":9}"
"{""data"":""Alex"",""created_at"":1726063432,""last_modified"":1726063432,""field_type"":0}","{""reminder_id"":"""",""data"":""1725292800"",""include_time"":true,""last_modified"":1726122448,""created_at"":1726122422,""is_range"":true,""end_timestamp"":""1725551940"",""field_type"":2}","{""field_type"":1,""last_modified"":1726063683,""created_at"":1726063683,""data"":""3560""}","{""created_at"":1726062561,""data"":""1.96"",""last_modified"":1726062561,""field_type"":1}","{""last_modified"":1726063952,""created_at"":1726063931,""data"":""al3x1343@protonmail.com"",""field_type"":6}","{""last_modified"":1726062375,""field_type"":5,""created_at"":1726062375,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""qNyr\"",\""name\"":\""finish reading book\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064616,""last_modified"":1726064616,""field_type"":7}",,"{""data"":""qnja"",""created_at"":1726065272,""last_modified"":1726065272,""field_type"":3}","{""created_at"":1726105180,""last_modified"":1726105180,""field_type"":4,""data"":""R9I7,1i4f""}","{""field_type"":8,""data"":""1726122448""}","{""field_type"":9,""data"":""1726060541""}"
"{""last_modified"":1726063478,""created_at"":1726063436,""field_type"":0,""data"":""Alexander""}",,"{""field_type"":1,""last_modified"":1726063691,""created_at"":1726063691,""data"":""2073""}","{""field_type"":1,""data"":""0.5"",""last_modified"":1726062577,""created_at"":1726062577}","{""last_modified"":1726063991,""field_type"":6,""created_at"":1726063991,""data"":""alexandernotthedra@gmail.com""}","{""field_type"":5,""last_modified"":1726062378,""created_at"":1726062377,""data"":""No""}",,,"{""created_at"":1726065291,""data"":""GSf_"",""last_modified"":1726065291,""field_type"":3}","{""last_modified"":1726105142,""created_at"":1726105133,""data"":""SEUo"",""field_type"":4}","{""field_type"":8,""data"":""1726105142""}","{""field_type"":9,""data"":""1726060542""}"
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://static.cdninstagram.com/rsrc.php/v3/ym/r/BQdTmxpRI6f.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
"{""field_type"":0,""last_modified"":1726063467,""data"":""Joanna"",""created_at"":1726063467}","{""include_time"":false,""end_timestamp"":""1727072893"",""is_range"":true,""last_modified"":1726122493,""created_at"":1726122483,""data"":""1726554493"",""field_type"":2,""reminder_id"":""""}","{""last_modified"":1726065463,""data"":""16470"",""field_type"":1,""created_at"":1726065463}","{""created_at"":1726062626,""field_type"":1,""last_modified"":1726062626,""data"":""-5.36""}","{""last_modified"":1726064069,""data"":""joannastrawberry29+hello@gmail.com"",""created_at"":1726064069,""field_type"":6}",,"{""field_type"":7,""created_at"":1726064444,""last_modified"":1726064460,""data"":""{\""options\"":[{\""id\"":\""ZxJz\"",\""name\"":\""post on Twitter\"",\""color\"":\""Purple\""},{\""id\"":\""upwi\"",\""name\"":\""watch Youtube videos\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""upwi\""]}""}",,"{""created_at"":1726065317,""last_modified"":1726065317,""field_type"":3,""data"":""qnja""}","{""field_type"":4,""last_modified"":1726105173,""data"":""uRAO,yORP"",""created_at"":1726105170}","{""data"":""1726122493"",""field_type"":8}","{""data"":""1726060545"",""field_type"":9}"
"{""last_modified"":1726063457,""created_at"":1726063457,""data"":""George"",""field_type"":0}","{""include_time"":true,""reminder_id"":"""",""field_type"":2,""is_range"":true,""created_at"":1726122521,""end_timestamp"":""1725829200"",""data"":""1725822900"",""last_modified"":1726122535}","{""last_modified"":1726065493,""field_type"":1,""data"":""9500"",""created_at"":1726065493}","{""last_modified"":1726062680,""created_at"":1726062680,""field_type"":1,""data"":""1.7""}","{""data"":""plgeorgebball@gmail.com"",""field_type"":6,""last_modified"":1726064087,""created_at"":1726064036}",,"{""last_modified"":1726064513,""data"":""{\""options\"":[{\""id\"":\""zy0x\"",\""name\"":\""game vs celtics\"",\""color\"":\""Purple\""},{\""id\"":\""WJsv\"",\""name\"":\""training\"",\""color\"":\""Purple\""},{\""id\"":\""w-f8\"",\""name\"":\""game vs spurs\"",\""color\"":\""Purple\""},{\""id\"":\""p1VQ\"",\""name\"":\""game vs knicks\"",\""color\"":\""Purple\""},{\""id\"":\""VjUA\"",\""name\"":\""recovery\"",\""color\"":\""Purple\""},{\""id\"":\""sQ8X\"",\""name\"":\""don't get injured\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064486,""field_type"":7}",,"{""field_type"":3,""last_modified"":1726065310,""data"":""qnja"",""created_at"":1726065310}","{""created_at"":1726105205,""field_type"":4,""last_modified"":1726105249,""data"":""R9I7,1i4f,yORP,SEUo""}","{""data"":""1726122535"",""field_type"":8}","{""field_type"":9,""data"":""1726060546""}"
"{""data"":""Judy"",""created_at"":1726063475,""field_type"":0,""last_modified"":1726063487}","{""end_timestamp"":"""",""reminder_id"":"""",""data"":""1726640950"",""field_type"":2,""include_time"":false,""created_at"":1726122550,""last_modified"":1726122550,""is_range"":false}",,,"{""created_at"":1726063882,""field_type"":6,""last_modified"":1726064000,""data"":""judysmithjr@outlook.com""}","{""last_modified"":1726062712,""field_type"":5,""data"":""Yes"",""created_at"":1726062712}","{""created_at"":1726064549,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""j8cC\"",\""name\"":\""finish training\"",\""color\"":\""Purple\""},{\""id\"":\""SmSk\"",\""name\"":\""brainwash\"",\""color\"":\""Purple\""},{\""id\"":\""mnf5\"",\""name\"":\""welcome to ba sing se\"",\""color\"":\""Purple\""},{\""id\"":\""hcrj\"",\""name\"":\""don't mess up\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""j8cC\"",\""SmSk\"",\""mnf5\"",\""hcrj\""]}"",""last_modified"":1726064591}",,,"{""field_type"":4,""last_modified"":1726105152,""created_at"":1726105152,""data"":""R9I7""}","{""field_type"":8,""data"":""1726122550""}","{""field_type"":9,""data"":""1726060549""}"
@@ -1,19 +0,0 @@
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'empty_test.dart' as preset_af_cloud_env_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;
Future<void> main() async {
preset_af_cloud_env_test.main();
appflowy_cloud_auth_test.main();
user_sync_test.main();
anon_user_continue_test.main();
// workspace
collaboration_workspace_test.main();
change_workspace_name_and_icon_test.main();
}
@@ -1,93 +0,0 @@
// import 'package:appflowy/env/cloud_env.dart';
// import 'package:appflowy/workspace/application/settings/prelude.dart';
// import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
// import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:integration_test/integration_test.dart';
// import '../shared/util.dart';
// void main() {
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// group('supabase auth', () {
// testWidgets('sign in with supabase', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
// });
// testWidgets('sign out with supabase', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.account);
// await tester.logout();
// // Go to the sign in page again
// await tester.pumpAndSettle(const Duration(seconds: 1));
// tester.expectToSeeGoogleLoginButton();
// });
// testWidgets('sign in as anonymous', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapSignInAsGuest();
// // should not see the sync setting page when sign in as anonymous
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.account);
// // Scroll to sign-out
// await tester.scrollUntilVisible(
// find.byType(SignInOutButton),
// 100,
// scrollable: find.findSettingsScrollable(),
// );
// await tester.tapButton(find.byType(SignInOutButton));
// tester.expectToSeeGoogleLoginButton();
// });
// // testWidgets('enable encryption', (tester) async {
// // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
// // await tester.tapGoogleLoginInButton();
// // // Open the setting page and sign out
// // await tester.openSettings();
// // await tester.openSettingsPage(SettingsPage.cloud);
// // // the switch should be off by default
// // tester.assertEnableEncryptSwitchValue(false);
// // await tester.toggleEnableEncrypt();
// // // the switch should be on after toggling
// // tester.assertEnableEncryptSwitchValue(true);
// // // the switch can not be toggled back to off
// // await tester.toggleEnableEncrypt();
// // tester.assertEnableEncryptSwitchValue(true);
// // });
// testWidgets('enable sync', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.cloud);
// // the switch should be on by default
// tester.assertSupabaseEnableSyncSwitchValue(true);
// await tester.toggleEnableSync(SupabaseEnableSync);
// // the switch should be off
// tester.assertSupabaseEnableSyncSwitchValue(false);
// // the switch should be on after toggling
// await tester.toggleEnableSync(SupabaseEnableSync);
// tester.assertSupabaseEnableSyncSwitchValue(true);
// });
// });
// }
@@ -1,100 +0,0 @@
// 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/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();
// final email = '${uuid()}@appflowy.io';
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;
// }
// await tester.initializeAppFlowy(
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
// email: email,
// );
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
// const name = 'AppFlowy.IO';
// // the workspace will be opened after created
// await tester.createCollaborativeWorkspace(name);
// final loading = find.byType(Loading);
// await tester.pumpUntilNotFound(loading);
// Finder success;
// final Finder items = find.byType(WorkspaceMenuItem);
// // 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,
// );
// 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);
// },
// );
});
});
}
@@ -25,6 +25,7 @@ void main() {
name: fieldName,
layout: ViewLayoutPB.Board,
);
await tester.dismissRowDetailPage();
await tester.tapButton(card1);
await tester.changeFieldTypeOfFieldWithName(
fieldName,
@@ -1,17 +1,21 @@
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board group test', () {
group('board group test:', () {
testWidgets('move row to another group', (tester) async {
const card1Name = 'Card 1';
await tester.initializeAppFlowy();
@@ -46,5 +50,105 @@ void main() {
final card1StatusText = tester.widget<Text>(card1StatusFinder).data;
expect(card1StatusText, 'Doing');
});
testWidgets('rename group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final headers = find.byType(BoardColumnHeader);
expect(headers, findsNWidgets(4));
// try to tap no status
final noStatus = headers.first;
expect(
find.descendant(of: noStatus, matching: find.text("No Status")),
findsOneWidget,
);
await tester.tapButton(noStatus);
expect(
find.descendant(of: noStatus, matching: find.byType(TextField)),
findsNothing,
);
// tap on To Do and edit it
final todo = headers.at(1);
expect(
find.descendant(of: todo, matching: find.text("To Do")),
findsOneWidget,
);
await tester.tapButton(todo);
await tester.enterText(
find.descendant(of: todo, matching: find.byType(TextField)),
"tada",
);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
final newHeaders = find.byType(BoardColumnHeader);
expect(newHeaders, findsNWidgets(4));
final tada = find.byType(BoardColumnHeader).at(1);
expect(
find.descendant(of: tada, matching: find.byType(TextField)),
findsNothing,
);
expect(
find.descendant(
of: tada,
matching: find.text("tada"),
),
findsOneWidget,
);
});
testWidgets('edit select option from row detail', (tester) async {
const card1Name = 'Card 1';
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
await tester.tapButton(
find.descendant(
of: find.byType(RowCard),
matching: find.text(card1Name),
),
);
await tester.tapGridFieldWithNameInRowDetailPage("Status");
await tester.tapButton(
find.byWidgetPredicate(
(widget) =>
widget is SelectOptionTagCell && widget.option.name == "To Do",
),
);
final editor = find.byType(SelectOptionEditor);
await tester.enterText(
find.descendant(of: editor, matching: find.byType(TextField)),
"tada",
);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.dismissFieldEditor();
await tester.dismissRowDetailPage();
final newHeaders = find.byType(BoardColumnHeader);
expect(newHeaders, findsNWidgets(4));
final tada = find.byType(BoardColumnHeader).at(1);
expect(
find.descendant(of: tada, matching: find.byType(TextField)),
findsNothing,
);
expect(
find.descendant(
of: tada,
matching: find.text("tada"),
),
findsOneWidget,
);
});
});
}
@@ -1,13 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
@@ -77,47 +78,47 @@ void main() {
shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length;
expect(shownGroups, 4);
});
});
testWidgets('delete a group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
testWidgets('delete a group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
// tap group option button for the first group. Delete shouldn't show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.first,
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
// tap group option button for the first group. Delete shouldn't show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.first,
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
// dismiss the popup
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// dismiss the popup
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// tap group option button for the first group. Delete should show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.at(1),
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
// tap group option button for the first group. Delete should show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.at(1),
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
// Tap the delete button and confirm
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
await tester.tapDialogOkButton();
// Tap the delete button and confirm
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
await tester.tapButtonWithName(LocaleKeys.space_delete.tr());
// Expect number of groups to decrease by one
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
// Expect number of groups to decrease by one
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
});
});
}
@@ -29,7 +29,7 @@ void main() {
},
);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.tapOKButton();
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
expect(find.text(name), findsNothing);
});
@@ -51,6 +51,37 @@ void main() {
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
});
testWidgets('duplicate item in ToDo card then delete', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
const name = 'Card 1';
final card1 = find.text(name);
await tester.hoverOnWidget(
card1,
onHover: () async {
final moreOption = find.byType(MoreCardOptionsAccessory);
await tester.tapButton(moreOption);
},
);
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
// get the last widget that contains the name
final duplicatedCard = find.textContaining(name, findRichText: true).last;
await tester.hoverOnWidget(
duplicatedCard,
onHover: () async {
final moreOption = find.byType(MoreCardOptionsAccessory);
await tester.tapButton(moreOption);
},
);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
expect(find.textContaining(name, findRichText: true), findsNWidgets(1));
});
testWidgets('add new group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
import 'board_add_row_test.dart' as board_add_row_test;
import 'board_group_test.dart' as board_group_test;
import 'board_row_test.dart' as board_row_test;
import 'board_field_test.dart' as board_field_test;
import 'board_hide_groups_test.dart' as board_hide_groups_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -11,4 +13,6 @@ void main() {
board_row_test.main();
board_add_row_test.main();
board_group_test.main();
board_field_test.main();
board_hide_groups_test.main();
}
@@ -0,0 +1,22 @@
import 'document/document_test_runner.dart' as document_test_runner;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'uncategorized/uncategorized_test_runner.dart'
as uncategorized_test_runner;
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
import 'data_migration/data_migration_test_runner.dart' as data_migration_test_runner;
Future<void> main() async {
data_migration_test_runner.main();
// uncategorized
uncategorized_test_runner.main();
// workspace
workspace_test_runner.main();
// document
document_test_runner.main();
// sidebar
sidebar_move_page_test.main();
}
@@ -24,24 +24,28 @@ import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.dart';
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('appflowy cloud', () {
testWidgets('anon user and then sign in', (tester) async {
testWidgets('anon user -> sign in -> open imported space', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
tester.expectToSeeText(LocaleKeys.signIn_loginStartWithAnonymous.tr());
await tester.tapContinousAnotherWay();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: pageName);
tester.expectToSeePageName(pageName);
// rename the name of the anon user
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
@@ -60,33 +64,17 @@ void main() {
// sign up with Google
await tester.tapGoogleLoginInButton();
// await tester.pumpAndSettle(const Duration(seconds: 16));
// sign out
// open the imported space
await tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
await tester.clickSpaceHeader();
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
// After import the anon user data, we will create a new space for it
await tester.openSpace("Getting started");
await tester.openPage(pageName);
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');
});
});
}
@@ -0,0 +1,5 @@
import 'anon_user_data_migration_test.dart' as anon_user_test;
void main() async {
anon_user_test.main();
}
@@ -0,0 +1,146 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document option actions:', () {
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);
});
// Copy link to block feature is disable temporarily, enable this test when the feature is ready.
// testWidgets('copy block link', (tester) async {
// await tester.initializeAppFlowy(
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
// );
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
// // open getting started page
// await tester.openPage(Constants.gettingStartedPageName);
// // hover and click on the option menu button beside the block component.
// await tester.editor.hoverAndClickOptionMenuButton([0]);
// // click the copy link to block option
// await tester.tap(
// find.findTextInFlowyText(
// LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),
// ),
// );
// await tester.pumpAndSettle(Durations.short1);
// // check the clipboard
// final content = await Clipboard.getData(Clipboard.kTextPlain);
// expect(
// content?.text,
// matches(
// r'^https:\/\/appflowy\.com\/app\/[a-f0-9-]{36}\/[a-f0-9-]{36}\?blockId=[A-Za-z0-9_-]+$',
// ),
// );
// });
testWidgets('hover on the block and delete it', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
// before delete
final path = [1];
final beforeDeletedBlock = tester.editor.getNodeAtPath(path);
// hover on the block and delete it
final optionButton = find.byWidgetPredicate(
(widget) =>
widget is DraggableOptionButton &&
widget.blockComponentContext.node.path.equals(path),
);
await tester.hoverOnWidget(
optionButton,
onHover: () async {
// click the delete button
await tester.tapButton(optionButton);
},
);
await tester.pumpAndSettle(Durations.short1);
// click the delete button
final deleteButton =
find.findTextInFlowyText(LocaleKeys.button_delete.tr());
await tester.tapButton(deleteButton);
// wait for the deletion
await tester.pumpAndSettle(Durations.short1);
// check if the block is deleted
final afterDeletedBlock = tester.editor.getNodeAtPath([1]);
expect(afterDeletedBlock.id, isNot(equals(beforeDeletedBlock.id)));
});
});
}
@@ -0,0 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'document_option_actions_test.dart' as document_option_actions_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
document_option_actions_test.main();
}
@@ -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);
});
});
}
@@ -15,8 +15,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -86,10 +86,13 @@ void main() {
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.cloud);
await tester.pumpAndSettle();
// the switch should be on by default
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
// wait for the switch animation
await tester.wait(250);
// the switch should be off
tester.assertAppFlowyCloudEnableSyncSwitchValue(false);
@@ -97,6 +100,7 @@ void main() {
// the switch should be on after toggling
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
await tester.wait(250);
});
});
}
@@ -17,9 +17,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -2,7 +2,7 @@ import 'package:appflowy/env/cloud_env.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../shared/util.dart';
import '../../../shared/util.dart';
// This test is meaningless, just for preventing the CI from failing.
void main() {
@@ -0,0 +1,9 @@
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
import 'user_setting_sync_test.dart' as user_sync_test;
void main() async {
preset_af_cloud_env_test.main();
appflowy_cloud_auth_test.main();
user_sync_test.main();
}
@@ -22,12 +22,12 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.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';
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';
import '../../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -15,9 +15,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../../shared/workspace.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -0,0 +1,97 @@
// 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/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('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;
}
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);
final loading = find.byType(Loading);
await tester.pumpUntilNotFound(loading);
Finder success;
final Finder items = find.byType(WorkspaceMenuItem);
// 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,
);
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
await tester.hoverOnWidget(
secondWorkspace,
onHover: () async {
// click the more button
final moreButton = find.byType(WorkspaceMoreActionList);
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
// click the delete button
final deleteButton = find.text(LocaleKeys.button_delete.tr());
expect(deleteButton, findsOneWidget);
await tester.tapButton(deleteButton);
// see the delete confirm dialog
final confirm =
find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
expect(confirm, findsOneWidget);
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// delete success
success = find.text(LocaleKeys.workspace_createSuccess.tr());
await tester.pumpUntilFound(success);
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
},
);
});
});
}
@@ -0,0 +1,76 @@
// ignore_for_file: unused_import
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/plugins/shared/share/share_menu.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_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Share menu:', () {
testWidgets('share tab', (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 share button
await tester.tapShareButton();
// expect the share menu is shown
final shareMenu = find.byType(ShareMenu);
expect(shareMenu, findsOneWidget);
// click the copy link button
final copyLinkButton = find.textContaining(
LocaleKeys.button_copyLink.tr(),
);
await tester.tapButton(copyLinkButton);
// read the clipboard content
final clipboardContent = await getIt<ClipboardService>().getData();
final plainText = clipboardContent.plainText;
expect(
plainText,
startsWith(ShareConstants.shareBaseUrl),
);
final shareValues = plainText!
.replaceAll('${ShareConstants.shareBaseUrl}/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, isNotEmpty);
final pageId = shareValues[1];
expect(pageId, isNotEmpty);
});
});
}
@@ -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,
);
},
);
});
}
@@ -0,0 +1,15 @@
import 'package:integration_test/integration_test.dart';
import 'change_name_and_icon_test.dart' as change_name_and_icon_test;
import 'collaborative_workspace_test.dart' as collaborative_workspace_test;
import 'share_menu_test.dart' as share_menu_test;
import 'workspace_settings_test.dart' as workspace_settings_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
workspace_settings_test.main();
share_menu_test.main();
collaborative_workspace_test.main();
change_name_and_icon_test.main();
}
@@ -211,21 +211,21 @@ void main() {
await tester.toggleIncludeTime();
// Select a date
final today = DateTime.now();
await tester.selectDay(content: today.day);
final now = DateTime.now();
await tester.selectDay(content: now.day);
await tester.dismissCellEditor();
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y').format(today),
content: DateFormat('MMM dd, y').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
// Toggle include time
final now = DateTime.now();
// When toggling include time, the time value is from the previous existing date time, not the current time
await tester.toggleIncludeTime();
await tester.dismissCellEditor();
@@ -299,7 +299,7 @@ void main() {
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 1',
);
@@ -311,12 +311,12 @@ void main() {
await tester.createOption(name: 'tag 2');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 2',
);
await tester.assertNumberOfSelectedOptionsInGrid(
tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
@@ -328,12 +328,12 @@ void main() {
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 1',
);
await tester.assertNumberOfSelectedOptionsInGrid(
tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
@@ -345,7 +345,7 @@ void main() {
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
@@ -378,7 +378,7 @@ void main() {
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags.first,
);
@@ -393,13 +393,13 @@ void main() {
await tester.dismissCellEditor();
for (final tag in tags) {
await tester.findSelectOptionWithNameInGrid(
tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tag,
);
}
await tester.assertNumberOfSelectedOptionsInGrid(
tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(4),
);
@@ -413,7 +413,7 @@ void main() {
}
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
@@ -426,16 +426,16 @@ void main() {
await tester.selectOption(name: tags[3]);
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[1],
);
await tester.findSelectOptionWithNameInGrid(
tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[3],
);
await tester.assertNumberOfSelectedOptionsInGrid(
tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(2),
);
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.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';
@@ -10,7 +10,7 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database field settings', () {
group('grid field settings test:', () {
testWidgets('field visibility', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -50,6 +50,15 @@ void main() {
await tester.tapHidePropertyButtonInFieldEditor();
await tester.dismissRowDetailPage();
tester.noFieldWithName('New field 1');
// the field should still be sort and filter-able
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.RichText,
"New field 1",
);
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
});
});
}
@@ -15,7 +15,7 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid field editor:', () {
group('grid edit field test:', () {
testWidgets('rename existing field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -24,7 +24,6 @@ void main() {
// Invoke the field editor
await tester.tapGridFieldWithName('Name');
await tester.tapEditFieldButton();
await tester.renameField('hello world');
await tester.dismissFieldEditor();
@@ -33,6 +32,32 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit field icon', (tester) async {
const icon = 'artificial_intelligence/ai-upscale-spark';
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
tester.assertFieldSvg('Name', FieldType.RichText);
// choose specific icon
await tester.tapGridFieldWithName('Name');
await tester.changeFieldIcon(icon);
await tester.dismissFieldEditor();
tester.assertFieldCustomSvg('Name', icon);
// remove icon
await tester.tapGridFieldWithName('Name');
await tester.changeFieldIcon('');
await tester.dismissFieldEditor();
tester.assertFieldSvg('Name', FieldType.RichText);
await tester.pumpAndSettle();
});
testWidgets('update field type of existing field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -126,7 +151,7 @@ void main() {
await tester.dismissFieldEditor();
tester.findFieldWithName('Right');
// insert new field to the right
// insert new field to the left
await tester.tapGridFieldWithName('Type');
await tester.tapInsertFieldButton(left: true, name: "Left");
await tester.dismissFieldEditor();
@@ -245,7 +270,6 @@ void main() {
matching: find.byType(TextField),
);
await tester.enterText(inputField, text);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
@@ -357,5 +381,189 @@ void main() {
// content: DateFormat('dd/MM/y hh:mm a').format(modified),
// );
// });
testWidgets('select option transform', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Grid,
);
// invoke the field editor of existing Single-Select field Type
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// add some select options
await tester.tapAddSelectOptionButton();
for (final optionName in ['A', 'B', 'C']) {
final inputField = find.descendant(
of: find.byType(CreateOptionTextField),
matching: find.byType(TextField),
);
await tester.enterText(inputField, optionName);
await tester.testTextInput.receiveAction(TextInputAction.done);
}
await tester.dismissFieldEditor();
// select A in first row's cell under the Type field
await tester.tapCellInGrid(
rowIndex: 0,
fieldType: FieldType.SingleSelect,
);
await tester.selectOption(name: 'A');
await tester.dismissCellEditor();
tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0);
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.RichText);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: "A",
cellIndex: 1,
);
// add some random text in the second row
await tester.editCell(
rowIndex: 1,
fieldType: FieldType.RichText,
input: "random",
cellIndex: 1,
);
tester.assertCellContent(
rowIndex: 1,
fieldType: FieldType.RichText,
content: "random",
cellIndex: 1,
);
await tester.changeFieldTypeOfFieldWithName(
'Type',
FieldType.SingleSelect,
);
tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0);
tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 1,
matcher: findsNothing,
);
// create a new field for testing
await tester.createField(FieldType.RichText, name: 'Test');
// edit the first 2 rows
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: "E,F",
cellIndex: 1,
);
await tester.editCell(
rowIndex: 1,
fieldType: FieldType.RichText,
input: "G",
cellIndex: 1,
);
await tester.changeFieldTypeOfFieldWithName(
'Test',
FieldType.MultiSelect,
);
tester.assertMultiSelectOption(contents: ['E', 'F'], rowIndex: 0);
tester.assertMultiSelectOption(contents: ['G'], rowIndex: 1);
await tester.tapCellInGrid(
rowIndex: 2,
fieldType: FieldType.MultiSelect,
);
await tester.selectOption(name: 'G');
await tester.createOption(name: 'H');
await tester.dismissCellEditor();
tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0);
tester.assertMultiSelectOption(contents: ['G', 'H'], rowIndex: 2);
await tester.changeFieldTypeOfFieldWithName(
'Test',
FieldType.RichText,
);
tester.assertCellContent(
rowIndex: 2,
fieldType: FieldType.RichText,
content: "G,H",
cellIndex: 1,
);
await tester.changeFieldTypeOfFieldWithName(
'Test',
FieldType.MultiSelect,
);
tester.assertMultiSelectOption(contents: ['E', 'F'], rowIndex: 0);
tester.assertMultiSelectOption(contents: ['G'], rowIndex: 1);
tester.assertMultiSelectOption(contents: ['G', 'H'], rowIndex: 2);
});
testWidgets('date time transform', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
// create a date field
await tester.createField(FieldType.DateTime);
// edit the first date cell
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.toggleIncludeTime();
final now = DateTime.now();
await tester.selectDay(content: now.day);
await tester.dismissCellEditor();
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y HH:mm').format(now),
);
await tester.changeFieldTypeOfFieldWithName(
'Date',
FieldType.RichText,
);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: DateFormat('MMM dd, y HH:mm').format(now),
cellIndex: 1,
);
await tester.editCell(
rowIndex: 1,
fieldType: FieldType.RichText,
input: "Oct 5, 2024",
cellIndex: 1,
);
tester.assertCellContent(
rowIndex: 1,
fieldType: FieldType.RichText,
content: "Oct 5, 2024",
cellIndex: 1,
);
await tester.changeFieldTypeOfFieldWithName(
'Date',
FieldType.DateTime,
);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y').format(now),
// content: DateFormat('MMM dd, y HH:mm').format(now),
);
tester.assertCellContent(
rowIndex: 1,
fieldType: FieldType.DateTime,
content: "Oct 05, 2024",
);
});
});
}
@@ -1,17 +1,21 @@
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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 filter:', () {
testWidgets('add text filter', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// create a filter
await tester.tapDatabaseFilterButton();
@@ -19,68 +23,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 +94,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 +128,70 @@ 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();
});
testWidgets('add date filter', (tester) async {
await tester.openTestDatabase(v020GridFileName);
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.DateTime, 'date');
// By default, the condition of date filter is current day and time
tester.assertNumberOfRowsInGridPage(0);
await tester.tapFilterButtonInGrid('date');
await tester.tapDateFilterButtonInGrid();
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
tester.assertNumberOfRowsInGridPage(7);
await tester.tapDateFilterButtonInGrid();
await tester.tapDateFilterCondition(DateTimeFilterCondition.isEmpty);
tester.assertNumberOfRowsInGridPage(3);
await tester.pumpAndSettle();
});
testWidgets('add timestamp filter', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
await tester.createField(
FieldType.CreatedTime,
name: 'Created at',
);
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.CreatedTime,
'Created at',
);
await tester.pumpAndSettle();
tester.assertNumberOfRowsInGridPage(3);
await tester.tapFilterButtonInGrid('Created at');
await tester.tapDateFilterButtonInGrid();
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
tester.assertNumberOfRowsInGridPage(0);
await tester.pumpAndSettle();
});
@@ -4,9 +4,12 @@ import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -18,12 +21,13 @@ import 'package:path_provider/path_provider.dart';
import '../../shared/database_test_op.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('media type option in database', () {
testWidgets('add media field and add files', (tester) async {
testWidgets('add media field and add files two times', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -92,5 +96,217 @@ void main() {
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('add two files at once', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Prepare files for upload from local
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('delete files', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Prepare files for upload from local
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
// Tap on the three dots menu for the first RenderMedia
final mediaMenuFinder = find.descendant(
of: find.byType(RenderMedia),
matching: find.byFlowySvg(FlowySvgs.three_dots_s),
);
await tester.tap(mediaMenuFinder.first);
await tester.pumpAndSettle();
// Tap on the delete button
await tester.tap(find.text(LocaleKeys.grid_media_delete.tr()));
await tester.pumpAndSettle();
// Tap on Delete button in the confirmation dialog
await tester.tap(
find.descendant(
of: find.byType(SpaceCancelOrConfirmButton),
matching: find.text(LocaleKeys.grid_media_delete.tr()),
),
);
await tester.pumpAndSettle(const Duration(seconds: 1));
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('hide file names', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Prepare files for upload from local
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
await tester.dismissCellEditor();
await tester.pumpAndSettle();
// Open first row in row detail view then toggle hide file names
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to be shown
expect(find.text('sample.jpeg'), findsOneWidget);
expect(find.text('sample.gif'), findsOneWidget);
await tester.tapGridFieldWithNameInRowDetailPage('Type');
await tester.pumpAndSettle();
// Toggle hide file names
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
await tester.dismissRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to be hidden
expect(find.text('sample.jpeg'), findsNothing);
expect(find.text('sample.gif'), findsNothing);
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
});
}
@@ -0,0 +1,190 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/database_test_op.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database row cover', () {
testWidgets('add image to media field and check if cover is set (grid)',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Prepare file for upload from local
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(paths: [imagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
// Close cell editor
await tester.dismissCellEditor();
// Open first row in row detail view
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect a cover to be shown
expect(find.byType(RowCover), findsOneWidget);
// Remove the temp file
await Future.wait([file.delete()]);
});
testWidgets('add and remove cover from Row Detail Card', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Open first row in row detail view
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect no cover
expect(find.byType(RowCover), findsNothing);
// Hover on RowBanner to show Add Cover button
await tester.hoverRowBanner();
// Click on Add Cover button
await tester.tapAddCoverButton();
// Expect a cover to be shown - the default asset cover
expect(find.byType(RowCover), findsOneWidget);
// Tap on the delete cover button
await tester.tapButton(find.byType(DeleteCoverButton));
await tester.pumpAndSettle();
// Expect no cover to be shown
expect(find.byType(AFImage), findsNothing);
});
testWidgets('add and change cover and check in Board', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
await tester.pumpAndSettle();
// Open "Card 1"
await tester.tap(find.text('Card 1'), warnIfMissed: false);
await tester.pumpAndSettle();
// Expect no cover
expect(find.byType(RowCover), findsNothing);
// Hover on RowBanner to show Add Cover button
await tester.hoverRowBanner();
// Click on Add Cover button
await tester.tapAddCoverButton();
// Expect default cover to be shown
expect(find.byType(RowCover), findsOneWidget);
// Prepare image for upload from local
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(paths: [imagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Hover on RowBanner to show Change Cover button
await tester.hoverRowBanner();
// Tap on the change cover button
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
await tester.pumpAndSettle();
// Change tab to Upload tab
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_label.tr(),
);
// Tab on the upload button
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
// Expect one cover
expect(find.byType(RowCover), findsOneWidget);
// Expect the cover to be shown both in RowCover and in CardCover
expect(find.byType(AFImage), findsNWidgets(2));
// Dismiss Row Detail Page
await tester.dismissRowDetailPage();
// Expect a cover to be shown in CardCover
expect(
find.descendant(
of: find.byType(CardCover),
matching: find.byType(AFImage),
),
findsOneWidget,
);
// Remove the temp file
await Future.wait([file.delete()]);
});
});
}
@@ -1,12 +1,14 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -36,26 +38,7 @@ void main() {
await tester.assertDocumentExistInRowDetailPage();
});
testWidgets('add emoji', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// Create a new grid
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.tapEmoji('😀');
// After select the emoji, the EmojiButton will show up
await tester.tapButton(find.byType(EmojiButton));
});
testWidgets('update emoji', (tester) async {
testWidgets('add and update emoji', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -68,8 +51,18 @@ void main() {
await tester.openEmojiPicker();
await tester.tapEmoji('😀');
// Update existing selected emoji
await tester.tapButton(find.byType(EmojiButton));
// expect to find the emoji selected
final firstEmojiFinder = find.byWidgetPredicate(
(w) => w is FlowyText && w.text == '😀',
);
// There are 2 eomjis - one in the row banner and another in the primary cell
expect(firstEmojiFinder, findsNWidgets(2));
// Update existing selected emoji - tap on it to update
await tester.tapButton(find.byType(EmojiIconWidget));
await tester.pumpAndSettle();
await tester.tapEmoji('😅');
// The emoji already displayed in the row banner
@@ -96,7 +89,9 @@ void main() {
await tester.tapEmoji('😀');
// Remove the emoji
await tester.tapButton(find.byType(RemoveEmojiButton));
await tester.tapButton(find.byType(EmojiIconWidget));
await tester.tapButton(find.text(LocaleKeys.button_remove.tr()));
final emojiText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == '😀',
);
@@ -321,7 +316,7 @@ void main() {
await tester.tapRowDetailPageDeleteRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(2);
tester.assertNumberOfRowsInGridPage(2);
});
testWidgets('duplicate row', (tester) async {
@@ -338,7 +333,7 @@ void main() {
await tester.tapRowDetailPageDuplicateRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(4);
tester.assertNumberOfRowsInGridPage(4);
});
});
}
@@ -1,66 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('create row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateRowButtonInGrid();
// 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4);
await tester.pumpAndSettle();
});
testWidgets('create row from row menu of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonInRowMenuOfGrid();
// 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4);
await tester.pumpAndSettle();
});
testWidgets('delete row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.pumpAndSettle();
await tester.tapDeleteOnRowMenu();
await tester.pumpAndSettle();
// 3 initial rows - 1 deleted
await tester.assertNumberOfRowsInGridPage(2);
});
});
testWidgets('check number of row indicator in the initial grid',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.pumpAndSettle();
});
});
}
@@ -9,7 +9,7 @@ void main() {
group('database', () {
testWidgets('import v0.2.0 database data', (tester) async {
await tester.openV020database();
await tester.openTestDatabase(v020GridFileName);
// wait the database data is loaded
await tester.pumpAndSettle(const Duration(microseconds: 500));
@@ -115,7 +115,7 @@ void main() {
[],
];
for (final (index, contents) in multiSelectCells.indexed) {
await tester.assertMultiSelectOption(
tester.assertMultiSelectOption(
rowIndex: index,
contents: contents,
);
@@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -7,9 +8,9 @@ import '../../shared/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('add text sort', (tester) async {
await tester.openV020database();
group('grid sort:', () {
testWidgets('text sort', (tester) async {
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
@@ -37,7 +38,7 @@ void main() {
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Name');
await tester.tapEditSortConditionButtonByFieldName('Name');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'E',
@@ -60,7 +61,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>[
@@ -84,8 +85,8 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('add checkbox sort', (tester) async {
await tester.openV020database();
testWidgets('checkbox', (tester) async {
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@@ -111,7 +112,7 @@ void main() {
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Done');
await tester.tapEditSortConditionButtonByFieldName('Done');
await tester.tapSortByDescending();
for (final (index, content) in <bool>[
true,
@@ -134,8 +135,8 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('add number sort', (tester) async {
await tester.openV020database();
testWidgets('number', (tester) async {
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
@@ -162,7 +163,7 @@ void main() {
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('number');
await tester.tapEditSortConditionButtonByFieldName('number');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'12',
@@ -186,15 +187,15 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('add checkbox and number sort', (tester) async {
await tester.openV020database();
testWidgets('checkbox and number', (tester) async {
await tester.openTestDatabase(v020GridFileName);
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
// open the sort menu and sort checkbox by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Done');
await tester.tapEditSortConditionButtonByFieldName('Done');
await tester.tapSortByDescending();
for (final (index, content) in <bool>[
true,
@@ -220,7 +221,7 @@ void main() {
FieldType.Number,
'number',
);
await tester.tapSortButtonByName('number');
await tester.tapEditSortConditionButtonByFieldName('number');
await tester.tapSortByDescending();
// check checkbox cell order
@@ -266,14 +267,14 @@ 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');
// open the sort menu and sort checkbox by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Done');
await tester.tapEditSortConditionButtonByFieldName('Done');
await tester.tapSortByDescending();
// add another sort, this time by number descending
@@ -282,7 +283,7 @@ void main() {
FieldType.Number,
'number',
);
await tester.tapSortButtonByName('number');
await tester.tapEditSortConditionButtonByFieldName('number');
await tester.tapSortByDescending();
// check checkbox cell order
@@ -370,5 +371,101 @@ void main() {
);
}
});
testWidgets('edit field', (tester) async {
await tester.openTestDatabase(v020GridFileName);
// create a number sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
// check the number cell order
for (final (index, content) in <String>[
'-2',
'-1',
'0.1',
'0.2',
'1',
'2',
'10',
'11',
'12',
'',
].indexed) {
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
final textCells = <String>[
'B',
'A',
'C',
'D',
'E',
'',
'',
'',
'',
'',
];
for (final (index, content) in textCells.indexed) {
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
// edit the name of the number field
await tester.tapGridFieldWithName('number');
await tester.renameField('hello world');
await tester.dismissFieldEditor();
await tester.tapGridFieldWithName('hello world');
await tester.dismissFieldEditor();
// expect name to be changed as well
await tester.tapSortMenuInSettingBar();
final sortItem = find.ancestor(
of: find.text('hello world'),
matching: find.byType(DatabaseSortItem),
);
expect(sortItem, findsOneWidget);
// change the field type of the field to checkbox
await tester.tapGridFieldWithName('hello world');
await tester.changeFieldTypeOfFieldWithName(
'hello world',
FieldType.Checkbox,
);
// expect name to be changed as well
await tester.tapSortMenuInSettingBar();
expect(sortItem, findsOneWidget);
final newTextCells = <String>[
'A',
'B',
'C',
'D',
'E',
'',
'',
'',
'',
'',
];
for (final (index, content) in newTextCells.indexed) {
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
});
});
}
@@ -0,0 +1,72 @@
import 'dart:ui';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Editor AppLifeCycle tests', () {
testWidgets(
'Selection is added back after pausing AppFlowy',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final selection = Selection.single(path: [4], startOffset: 0);
await tester.editor.updateSelection(selection);
binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
expect(tester.editor.getCurrentEditorState().selection, null);
binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pumpAndSettle();
expect(tester.editor.getCurrentEditorState().selection, selection);
},
);
testWidgets(
'Null selection is retained after pausing AppFlowy',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final selection = Selection.single(path: [4], startOffset: 0);
await tester.editor.updateSelection(selection);
await tester.editor.updateSelection(null);
binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
expect(tester.editor.getCurrentEditorState().selection, null);
binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pumpAndSettle();
expect(tester.editor.getCurrentEditorState().selection, null);
},
);
testWidgets(
'Non-collapsed selection is retained after pausing AppFlowy',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final selection = Selection(
start: Position(path: [3]),
end: Position(path: [3], offset: 8),
);
await tester.editor.updateSelection(selection);
binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pumpAndSettle();
expect(tester.editor.getCurrentEditorState().selection, selection);
},
);
});
}
@@ -13,13 +13,15 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('paste in codeblock', () {
group('paste in codeblock:', () {
testWidgets('paste multiple lines in codeblock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent();
await tester.createNewPageWithNameUnderParent(name: 'Test Document');
// focus on the editor
await tester.tapButton(find.byType(AppFlowyEditor));
// mock the clipboard
const lines = 3;
@@ -1,12 +1,19 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.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/link_preview/custom_link_preview.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/services.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../shared/util.dart';
@@ -207,7 +214,7 @@ void main() {
final image = await rootBundle.load('assets/test/images/sample.png');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('png', bytes), (editorState) {
expect(editorState.document.root.children.length, 2);
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
@@ -218,7 +225,7 @@ void main() {
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
expect(editorState.document.root.children.length, 2);
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
@@ -226,15 +233,14 @@ void main() {
});
testWidgets('paste image(gif) from memory', (tester) async {
// It's not supported yet.
// final image = await rootBundle.load('assets/test/images/sample.gif');
// final bytes = image.buffer.asUint8List();
// await tester.pasteContent(image: ('gif', bytes), (editorState) {
// expect(editorState.document.root.children.length, 2);
// final node = editorState.getNodeAtPath([0])!;
// expect(node.type, ImageBlockKeys.type);
// expect(node.attributes[ImageBlockKeys.url], isNotNull);
// });
final image = await rootBundle.load('assets/test/images/sample.gif');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('gif', bytes), (editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
});
testWidgets(
@@ -280,7 +286,7 @@ void main() {
html: html,
image: ('png', bytes),
(editorState) {
expect(editorState.document.root.children.length, 2);
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
},
@@ -312,14 +318,155 @@ void main() {
'auto convert url to link preview block',
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) {
await tester.pasteContent(plainText: url, (editorState) async {
// the second one is the paragraph node
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
// hover on the link preview block
// click the more button
// and select convert to link
await tester.hoverOnWidget(
find.byType(CustomLinkPreviewWidget),
onHover: () async {
final convertToLinkButton = find.byWidgetPredicate((widget) {
return widget is MenuBlockButton &&
widget.tooltip ==
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
});
expect(convertToLinkButton, findsOneWidget);
await tester.tap(convertToLinkButton);
await tester.pumpAndSettle();
},
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final textNode = editorState.getNodeAtPath([0])!;
expect(textNode.type, ParagraphBlockKeys.type);
expect(textNode.delta!.toJson(), [
{
'insert': url,
'attributes': {'href': url},
}
]);
},
);
testWidgets(
'ctrl/cmd+z to undo the auto convert url to link preview block',
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) async {
// the second one is the paragraph node
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
await tester.editor.tapLineOfEditorAt(0);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{
'insert': url,
'attributes': {'href': url},
}
]);
},
);
testWidgets(
'paste the nodes start with non-delta node',
(tester) async {
await tester.pasteContent((_) {});
const text = 'Hello World';
final editorState = tester.editor.getCurrentEditorState();
final transaction = editorState.transaction;
// [image_block]
// [paragraph_block]
transaction.insertNodes([
0,
], [
customImageNode(url: ''),
paragraphNode(text: text),
]);
await editorState.apply(transaction);
await tester.pumpAndSettle();
await tester.editor.tapLineOfEditorAt(0);
// select all and copy
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
// put the cursor to the end of the paragraph block
await tester.editor.tapLineOfEditorAt(0);
// paste the content
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
// expect the image and the paragraph block are inserted below the cursor
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
},
);
testWidgets('paste the url without protocol', (tester) async {
// paste the image that from local file
const plainText = '1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
(editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});
testWidgets('paste the image url', (tester) async {
const plainText = 'https://appflowy.io/1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
(editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});
}
extension on WidgetTester {
@@ -335,7 +482,9 @@ extension on WidgetTester {
await tapAnonymousSignInButton();
// create a new document
await createNewPageWithNameUnderParent();
await createNewPageWithNameUnderParent(name: 'Test Document');
// tap the editor
await tapButton(find.byType(AppFlowyEditor));
await beforeTest?.call(editor.getCurrentEditorState());
@@ -1,6 +1,4 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -17,12 +15,11 @@ void main() {
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent();
const pageName = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: pageName);
// expect to see a new document
tester.expectToSeePageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
tester.expectToSeePageName(pageName);
// and with one paragraph block
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
});
@@ -0,0 +1,61 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('customer:', () {
testWidgets('backtick issue - inline code', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const pageName = 'backtick issue';
await tester.createNewPageWithNameUnderParent(name: pageName);
// focus on the editor
await tester.tap(find.byType(AppFlowyEditor));
// input backtick
const text = '`Hello` AppFlowy';
for (var i = 0; i < text.length; i++) {
await tester.ime.insertCharacter(text[i]);
}
final node = tester.editor.getNodeAtPath([0]);
expect(
node.delta?.toJson(),
equals([
{
"insert": "Hello",
"attributes": {"code": true},
},
{"insert": " AppFlowy"},
]),
);
});
testWidgets('backtick issue - inline code', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const pageName = 'backtick issue';
await tester.createNewPageWithNameUnderParent(name: pageName);
// focus on the editor
await tester.tap(find.byType(AppFlowyEditor));
// input backtick
const text = '```';
for (var i = 0; i < text.length; i++) {
await tester.ime.insertCharacter(text[i]);
}
final node = tester.editor.getNodeAtPath([0]);
expect(node.type, equals(CodeBlockKeys.type));
});
});
}
@@ -0,0 +1,51 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document selection:', () {
testWidgets('select text from start to end by pan gesture ',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent();
final editor = tester.editor;
final editorState = editor.getCurrentEditorState();
// insert a paragraph
final transaction = editorState.transaction;
transaction.insertNode(
[0],
paragraphNode(
text:
'''Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.''',
),
);
await editorState.apply(transaction);
await tester.pumpAndSettle(Durations.short1);
final textBlocks = find.byType(AppFlowyRichText);
final topLeft = tester.getTopLeft(textBlocks.at(0));
final gesture = await tester.startGesture(
topLeft,
pointer: 7,
);
await tester.pumpAndSettle();
for (var i = 0; i < 10; i++) {
await gesture.moveBy(const Offset(10, 0));
await tester.pump(Durations.short1);
}
expect(editorState.selection!.start.offset, 0);
});
});
}
@@ -0,0 +1,140 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document shortcuts:', () {
testWidgets('custom cut command', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const pageName = 'Test Document Shortcuts';
await tester.createNewPageWithNameUnderParent(name: pageName);
// focus on the editor
await tester.tap(find.byType(AppFlowyEditor));
// mock the data
final editorState = tester.editor.getCurrentEditorState();
final transaction = editorState.transaction;
const text1 = '1. First line';
const text2 = '2. Second line';
transaction.insertNodes([
0,
], [
paragraphNode(text: text1),
paragraphNode(text: text2),
]);
await editorState.apply(transaction);
await tester.pumpAndSettle();
// focus on the end of the first line
await tester.editor.updateSelection(
Selection.collapsed(
Position(path: [0], offset: text1.length),
),
);
// press the keybinding
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
// check the clipboard
final clipboard = await Clipboard.getData(Clipboard.kTextPlain);
expect(
clipboard?.text,
equals(text1),
);
final node = tester.editor.getNodeAtPath([0]);
expect(
node.delta?.toPlainText(),
equals(text2),
);
// select the whole line
await tester.editor.updateSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: text2.length,
),
);
// press the keybinding
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
// all the text should be deleted
expect(
node.delta?.toPlainText(),
equals(''),
);
final clipboard2 = await Clipboard.getData(Clipboard.kTextPlain);
expect(
clipboard2?.text,
equals(text2),
);
});
testWidgets(
'custom copy command - copy whole line when selection is collapsed',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const pageName = 'Test Document Shortcuts';
await tester.createNewPageWithNameUnderParent(name: pageName);
// focus on the editor
await tester.tap(find.byType(AppFlowyEditor));
// mock the data
final editorState = tester.editor.getCurrentEditorState();
final transaction = editorState.transaction;
const text1 = '1. First line';
transaction.insertNodes([
0,
], [
paragraphNode(text: text1),
]);
await editorState.apply(transaction);
await tester.pumpAndSettle();
// focus on the end of the first line
await tester.editor.updateSelection(
Selection.collapsed(
Position(path: [0], offset: text1.length),
),
);
// press the keybinding
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
// check the clipboard
final clipboard = await Clipboard.getData(Clipboard.kTextPlain);
expect(
clipboard?.text,
equals(text1),
);
});
});
}
@@ -0,0 +1,607 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
// Test cases for the Document SubPageBlock that needs to be covered:
// - [x] Insert a new SubPageBlock from Slash menu items (Expect it will create a child view under current view)
// - [x] Delete a SubPageBlock from Block Action Menu (Expect the view is moved to trash / deleted)
// - [x] Delete a SubPageBlock with backspace when selected (Expect the view is moved to trash / deleted)
// - [x] Copy+paste a SubPageBlock in same Document (Expect a new view is created under current view with same content and name)
// - [x] Copy+paste a SubPageBlock in different Document (Expect a new view is created under current view with same content and name)
// - [x] Cut+paste a SubPageBlock in same Document (Expect the view to be deleted on Cut, and brought back on Paste)
// - [x] Cut+paste a SubPageBlock in different Document (Expect the view to be deleted on Cut, and brought back on Paste)
// - [x] Undo adding a SubPageBlock (Expect the view to be deleted)
// - [x] Undo delete of a SubPageBlock (Expect the view to be brought back to original position)
// - [x] Redo adding a SubPageBlock (Expect the view to be restored)
// - [x] Redo delete of a SubPageBlock (Expect the view to be moved to trash again)
// - [x] Renaming a child view (Expect the view name to be updated in the document)
// - [x] Deleting a view (to trash) linked to a SubPageBlock shows a hint that the view is in trash (Expect a hint to be shown)
// - [x] Deleting a view (in trash) linked to a SubPageBlock deletes the SubPageBlock (Expect the SubPageBlock to be deleted)
// - [x] Duplicating a SubPageBlock node from Action Menu (Expect a new view is created under current view with same content and name + (copy))
// - [x] Dragging a SubPageBlock node to a new position in the document (Expect everything to be normal)
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Document SubPageBlock tests', () {
testWidgets('Insert a new SubPageBlock from Slash menu items',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
findsNWidgets(3),
);
});
testWidgets('Rename and then Delete a SubPageBlock from Block Action Menu',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNothing);
});
testWidgets('Copy+paste a SubPageBlock in same Document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
await tester.editor.tapLineOfEditorAt(1);
// This is a workaround to allow CTRL+A and CTRL+C to work to copy
// the SubPageBlock as well.
await tester.ime.insertText('ABC');
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.editor.hoverAndClickOptionAddButton([1], false);
await tester.editor.tapLineOfEditorAt(2);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle(const Duration(seconds: 5));
expect(find.byType(SubPageBlockComponent), findsNWidgets(2));
expect(find.text('Child page'), findsNWidgets(2));
expect(find.text('Child page (copy)'), findsNWidgets(2));
});
testWidgets('Copy+paste a SubPageBlock in different Document',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
await tester.editor.tapLineOfEditorAt(1);
// This is a workaround to allow CTRL+A and CTRL+C to work to copy
// the SubPageBlock as well.
await tester.ime.insertText('ABC');
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock-2');
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock-2',
layout: ViewLayoutPB.Document,
);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(find.text('Child page'), findsOneWidget);
expect(find.text('Child page (copy)'), findsNWidgets(2));
});
testWidgets('Cut+paste a SubPageBlock in same Document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
.updateSelection(Selection.single(path: [0], startOffset: 0));
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
expect(find.text('Child page'), findsNothing);
await tester.editor.tapLineOfEditorAt(0);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(find.text('Child page'), findsNWidgets(2));
});
testWidgets('Cut+paste a SubPageBlock in different Document',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
.updateSelection(Selection.single(path: [0], startOffset: 0));
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
expect(find.text('Child page'), findsNothing);
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock-2');
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock-2',
layout: ViewLayoutPB.Document,
);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(find.text('Child page'), findsNWidgets(2));
expect(find.text('Child page (copy)'), findsNothing);
});
testWidgets('Undo adding a SubPageBlock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.tapLineOfEditorAt(0);
// Undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
expect(find.text('Child page'), findsNothing);
});
testWidgets('Undo delete of a SubPageBlock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNothing);
expect(find.byType(SubPageBlockComponent), findsNothing);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
});
// Redo: undoing adding a subpage block, then redoing to bring it back
// -> Add a subpage block
// -> Undo
// -> Redo
testWidgets('Redo adding of a SubPageBlock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.tapLineOfEditorAt(0);
// Undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
expect(find.text('Child page'), findsNothing);
// Redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isShiftPressed: true,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(find.text('Child page'), findsNWidgets(2));
});
// Redo: undoing deleting a subpage block, then redoing to delete it again
// -> Add a subpage block
// -> Delete
// -> Undo
// -> Redo
testWidgets('Redo delete of a SubPageBlock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
// Delete
await tester.editor.hoverAndClickOptionMenuButton([1]);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNothing);
expect(find.byType(SubPageBlockComponent), findsNothing);
await tester.editor.tapLineOfEditorAt(0);
// Undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(find.text('Child page'), findsNWidgets(2));
// Redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isShiftPressed: true,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
expect(find.text('Child page'), findsNothing);
});
testWidgets('Delete a view first to trash, then from trash',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
final hintText = LocaleKeys.document_plugins_subPage_inTrashHint.tr();
expect(find.text(hintText), findsNothing);
await tester.hoverOnPageName('Child page');
await tester.tapDeletePageButton();
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text(hintText), findsOne);
// Go to trash
await tester.tapTrashButton();
await tester.pumpAndSettle();
// Tap on delete all button
await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr()));
await tester.pumpAndSettle();
// Tap ok to delete app pages in trash
await tester.tap(find.text(LocaleKeys.button_delete.tr()));
await tester.pumpAndSettle();
await tester.openPage('SubPageBlock');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNothing);
expect(find.byType(SubPageBlockComponent), findsNothing);
});
testWidgets('Duplicate SubPageBlock from Block Menu', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
expect(find.text('Child page (copy)'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsNWidgets(2));
});
testWidgets('Drag SubPageBlock to top of Document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
final beforeNode = tester.editor.getNodeAtPath([1]);
await tester.editor.dragBlock([1], const Offset(20, -45));
await tester.pumpAndSettle(Durations.long1);
final afterNode = tester.editor.getNodeAtPath([0]);
expect(afterNode.type, SubPageBlockKeys.type);
expect(afterNode.type, beforeNode.type);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(
find.text(LocaleKeys.document_plugins_subPage_inTrashHint.tr()),
findsNothing,
);
});
});
}
extension _SubPageTestHelper on WidgetTester {
Future<void> insertSubPageFromSlashMenu([bool withTextNode = false]) async {
await editor.tapLineOfEditorAt(0);
if (withTextNode) {
await ime.insertText('ABC');
await editor.getCurrentEditorState().insertNewLine();
await pumpAndSettle();
}
await editor.showSlashMenu();
await editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_subPage_name.tr(),
offset: 100,
);
await pumpUntilFound(find.byType(SubPageBlockComponent));
}
}
@@ -5,10 +5,11 @@ import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
import 'document_create_and_delete_test.dart'
as document_create_and_delete_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_inline_page_reference_test.dart'
as document_inline_page_reference_test;
import 'document_more_actions_test.dart' as document_more_actions_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_shortcuts_test.dart' as document_shortcuts_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
import 'document_with_database_test.dart' as document_with_database_test;
@@ -45,4 +46,5 @@ void startTesting() {
document_inline_page_reference_test.main();
document_more_actions_test.main();
document_with_file_test.main();
document_shortcuts_test.main();
}
@@ -0,0 +1,252 @@
import 'package:appflowy_editor/appflowy_editor.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 'package:universal_platform/universal_platform.dart';
import '../../shared/constants.dart';
import '../../shared/util.dart';
const _testDocumentName = 'Test Document';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document title:', () {
testWidgets('create a new document and edit title', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
expect(title, findsOneWidget);
// input name
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
final newTitle = tester.editor.findDocumentTitle(_testDocumentName);
expect(newTitle, findsOneWidget);
// press enter to create a new line
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
const firstLine = 'First line of text';
await tester.ime.insertText(firstLine);
await tester.pumpAndSettle();
final firstLineText = find.text(firstLine, findRichText: true);
expect(firstLineText, findsOneWidget);
// press cmd/ctrl+left to move the cursor to the start of the line
if (UniversalPlatform.isMacOS) {
await tester.simulateKeyEvent(
LogicalKeyboardKey.arrowLeft,
isMetaPressed: true,
);
} else {
await tester.simulateKeyEvent(LogicalKeyboardKey.home);
}
await tester.pumpAndSettle();
// press arrow left to delete the first line
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
// check if the title is on focus
final titleOnFocus = tester.editor.findDocumentTitle(_testDocumentName);
final titleWidget = tester.widget<TextField>(titleOnFocus);
expect(titleWidget.focusNode?.hasFocus, isTrue);
// press the right arrow key to move the cursor to the first line
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowRight);
// check if the title is not on focus
expect(titleWidget.focusNode?.hasFocus, isFalse);
final editorState = tester.editor.getCurrentEditorState();
expect(editorState.selection, Selection.collapsed(Position(path: [0])));
// press the backspace key to go to the title
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
expect(editorState.selection, null);
expect(titleWidget.focusNode?.hasFocus, isTrue);
});
testWidgets('check if the title is saved', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
expect(title, findsOneWidget);
// input name
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
if (UniversalPlatform.isLinux) {
// wait for the name to be saved
await tester.wait(250);
}
// go to the get started page
await tester.tapButton(
tester.findPageName(Constants.gettingStartedPageName),
);
// go back to the page
await tester.tapButton(tester.findPageName(_testDocumentName));
// check if the title is saved
final testDocumentTitle = tester.editor.findDocumentTitle(
_testDocumentName,
);
expect(testDocumentTitle, findsOneWidget);
});
testWidgets('arrow up from first line moves focus to title',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.ime.insertText('First line of text');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.home);
// press the arrow upload
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp);
final titleWidget = tester.widget<TextField>(
tester.editor.findDocumentTitle(_testDocumentName),
);
expect(titleWidget.focusNode?.hasFocus, isTrue);
final editorState = tester.editor.getCurrentEditorState();
expect(editorState.selection, null);
});
testWidgets(
'backspace at start of first line moves focus to title and deletes empty paragraph',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
final editorState = tester.editor.getCurrentEditorState();
expect(editorState.document.root.children.length, equals(2));
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
final titleWidget = tester.widget<TextField>(
tester.editor.findDocumentTitle(_testDocumentName),
);
expect(titleWidget.focusNode?.hasFocus, isTrue);
// at least one empty paragraph node is created
expect(editorState.document.root.children.length, equals(1));
});
testWidgets('arrow right from end of title moves focus to first line',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.ime.insertText('First line of text');
await tester.tapButton(
tester.editor.findDocumentTitle(_testDocumentName),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.end);
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowRight);
final editorState = tester.editor.getCurrentEditorState();
expect(
editorState.selection,
Selection.collapsed(
Position(path: [0]),
),
);
});
testWidgets('change the title via sidebar, check the title is updated',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
expect(title, findsOneWidget);
await tester.hoverOnPageName(
'',
onHover: () async {
await tester.renamePage(_testDocumentName);
await tester.pumpAndSettle();
},
);
await tester.pumpAndSettle();
final newTitle = tester.editor.findDocumentTitle(_testDocumentName);
expect(newTitle, findsOneWidget);
});
testWidgets('press arrow down key in title, check if the cursor flashes',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
const inputText = 'Hello World';
await tester.ime.insertText(inputText);
await tester.tapButton(
tester.editor.findDocumentTitle(_testDocumentName),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown);
final editorState = tester.editor.getCurrentEditorState();
expect(
editorState.selection,
Selection.collapsed(
Position(path: [0], offset: inputText.length),
),
);
});
});
}
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -60,6 +60,57 @@ void main() {
);
});
testWidgets('insert multiple referenced boards', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new grid
final id = uuid();
final name = '${ViewLayoutPB.Board.name}_$id';
await tester.createNewPageWithNameUnderParent(
name: name,
layout: ViewLayoutPB.Board,
openAfterCreated: false,
);
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'insert_a_reference_${ViewLayoutPB.Board.name}',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a referenced view
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
ViewLayoutPB.Board.slashMenuLinkedName,
);
final referencedDatabase1 = find.descendant(
of: find.byType(InlineActionsHandler),
matching: find.findTextInFlowyText(name),
);
expect(referencedDatabase1, findsOneWidget);
await tester.tapButton(referencedDatabase1);
await tester.editor.tapLineOfEditorAt(1);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
ViewLayoutPB.Board.slashMenuLinkedName,
);
final referencedDatabase2 = find.descendant(
of: find.byType(InlineActionsHandler),
matching: find.findTextInFlowyText(name),
);
expect(referencedDatabase2, findsOneWidget);
await tester.tapButton(referencedDatabase2);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(DesktopBoardPage),
),
findsNWidgets(2),
);
});
testWidgets('insert a referenced calendar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -1,5 +1,7 @@
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';
@@ -8,7 +10,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy/startup/startup.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
@@ -36,10 +37,6 @@ void main() {
LocaleKeys.document_slashMenu_name_file.tr(),
);
expect(find.byType(FileBlockComponent), findsOneWidget);
await tester.tap(find.byType(FileBlockComponent));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(FileUploadMenu), findsOneWidget);
final image = await rootBundle.load('assets/test/images/sample.jpeg');
@@ -116,9 +113,6 @@ void main() {
LocaleKeys.document_slashMenu_name_file.tr(),
);
expect(find.byType(FileBlockComponent), findsOneWidget);
await tester.tap(find.byType(FileBlockComponent));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(FileUploadMenu), findsOneWidget);
// Navigate to integrate link tab
@@ -161,9 +161,8 @@ void main() {
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
'Unsplash',
);
await tester.tapButtonWithName('Unsplash');
await tester.pumpUntilFound(find.byType(UnsplashImageWidget));
expect(find.byType(UnsplashImageWidget), findsOneWidget);
});
});
@@ -0,0 +1,137 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:time/time.dart';
import '../../shared/database_test_op.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid edit row test:', () {
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));
await tester.editCell(
rowIndex: 4,
fieldType: FieldType.RichText,
input: "x",
);
await tester.pumpAndSettle(200.milliseconds);
final reSorted = [
unsorted[7],
unsorted[8],
unsorted[1],
unsorted[9],
unsorted[10],
unsorted[6],
unsorted[12],
unsorted[2],
unsorted[0],
unsorted[3],
unsorted[5],
unsorted[11],
unsorted[4],
];
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(reSorted));
// delete the sort
await tester.tapSortMenuInSettingBar();
await tester.tapDeleteAllSortsButton();
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unsorted));
});
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));
expect(actual.length, equals(7));
tester.assertNumberOfRowsInGridPage(7);
await tester.tapCheckboxCellInGrid(rowIndex: 0);
await tester.pumpAndSettle(200.milliseconds);
// verify grid data
actual = tester.getGridRows();
expect(actual.length, equals(6));
tester.assertNumberOfRowsInGridPage(6);
final edited = [
original[3],
original[5],
original[6],
original[7],
original[9],
original[12],
];
expect(actual, orderedEquals(edited));
// 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(13));
tester.assertNumberOfRowsInGridPage(13);
});
});
}
@@ -0,0 +1,120 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid simultaneous sort and filter test:', () {
// testWidgets('delete filter with active sort', (tester) async {
// await tester.openTestDatabase(v069GridFileName);
// // get grid data
// final original = tester.getGridRows();
// // add a filter
// await tester.tapDatabaseFilterButton();
// await tester.tapCreateFilterByFieldType(
// FieldType.Checkbox,
// 'Registration Complete',
// );
// // add a sort
// await tester.tapDatabaseSortButton();
// await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
// final filteredAndSorted = [
// original[7],
// original[1],
// original[9],
// original[6],
// original[12],
// original[3],
// original[5],
// ];
// // verify grid data
// List actual = tester.getGridRows();
// expect(actual, orderedEquals(filteredAndSorted));
// // delete the filter
// await tester.tapFilterButtonInGrid('Registration Complete');
// await tester
// .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
// await tester.tapDeleteFilterButtonInGrid();
// final sorted = [
// original[7],
// original[8],
// original[1],
// original[9],
// original[11],
// original[10],
// original[6],
// original[12],
// original[2],
// original[0],
// original[3],
// original[5],
// original[4],
// ];
// // verify grid data
// actual = tester.getGridRows();
// expect(actual, orderedEquals(sorted));
// });
testWidgets('delete sort with active fiilter', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
final filteredAndSorted = [
original[7],
original[1],
original[9],
original[6],
original[12],
original[3],
original[5],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filteredAndSorted));
// delete the sort
await tester.tapSortMenuInSettingBar();
await tester.tapDeleteAllSortsButton();
final filtered = [
original[1],
original[3],
original[5],
original[6],
original[7],
original[9],
original[12],
];
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
});
});
}
@@ -0,0 +1,183 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid reopen test:', () {
testWidgets('base case', (tester) async {
await tester.openTestDatabase(v069GridFileName);
final expected = tester.getGridRows();
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
final actual = tester.getGridRows();
expect(actual, orderedEquals(expected));
});
testWidgets('with sort configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final unsorted = tester.getGridRows();
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
final sorted = [
unsorted[7],
unsorted[8],
unsorted[1],
unsorted[9],
unsorted[11],
unsorted[10],
unsorted[6],
unsorted[12],
unsorted[2],
unsorted[0],
unsorted[3],
unsorted[5],
unsorted[4],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// delete sorts
// TODO(RS): Shouldn't the sort/filter list show automatically!?
await tester.tapDatabaseSortButton();
await tester.tapSortMenuInSettingBar();
await tester.tapDeleteAllSortsButton();
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unsorted));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unsorted));
});
testWidgets('with filter configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final unfiltered = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
final filtered = [
unfiltered[1],
unfiltered[3],
unfiltered[5],
unfiltered[6],
unfiltered[7],
unfiltered[9],
unfiltered[12],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
// delete the filter
// TODO(RS): Shouldn't the sort/filter list show automatically!?
await tester.tapDatabaseFilterButton();
await tester.tapFilterButtonInGrid('Registration Complete');
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unfiltered));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(unfiltered));
});
testWidgets('with both filter and sort configured', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
final filteredAndSorted = [
original[7],
original[1],
original[9],
original[6],
original[12],
original[3],
original[5],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filteredAndSorted));
// go to another page and come back
await tester.openPage('Getting started');
await tester.openPage('v069', layout: ViewLayoutPB.Grid);
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(filteredAndSorted));
});
});
}
@@ -0,0 +1,214 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
import 'grid_test_extensions.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid reorder row test:', () {
testWidgets('base case', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// reorder row
await tester.reorderRow(original[4], original[1]);
// verify grid data
List reordered = [
original[0],
original[4],
original[1],
original[2],
original[3],
original[5],
original[6],
original[7],
original[8],
original[9],
original[10],
original[11],
original[12],
];
List actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// reorder row
await tester.reorderRow(reordered[1], reordered[3]);
// verify grid data
reordered = [
original[0],
original[1],
original[2],
original[4],
original[3],
original[5],
original[6],
original[7],
original[8],
original[9],
original[10],
original[11],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// reorder row
await tester.reorderRow(reordered[2], reordered[0]);
// verify grid data
reordered = [
original[2],
original[0],
original[1],
original[4],
original[3],
original[5],
original[6],
original[7],
original[8],
original[9],
original[10],
original[11],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
});
testWidgets('with active sort', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
// verify grid data
final sorted = [
original[7],
original[8],
original[1],
original[9],
original[11],
original[10],
original[6],
original[12],
original[2],
original[0],
original[3],
original[5],
original[4],
];
List actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
// reorder row
await tester.reorderRow(original[4], original[1]);
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_cancel.tr());
// verify grid data
actual = tester.getGridRows();
expect(actual, orderedEquals(sorted));
});
testWidgets('with active filter', (tester) async {
await tester.openTestDatabase(v069GridFileName);
// get grid data
final original = tester.getGridRows();
// add a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.Checkbox,
'Registration Complete',
);
final filtered = [
original[1],
original[3],
original[5],
original[6],
original[7],
original[9],
original[12],
];
// verify grid data
List actual = tester.getGridRows();
expect(actual, orderedEquals(filtered));
// reorder row
await tester.reorderRow(filtered[3], filtered[1]);
// verify grid data
List reordered = [
original[1],
original[6],
original[3],
original[5],
original[7],
original[9],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// reorder row
await tester.reorderRow(reordered[3], reordered[5]);
// verify grid data
reordered = [
original[1],
original[6],
original[3],
original[7],
original[9],
original[5],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(reordered));
// delete the filter
await tester.tapFilterButtonInGrid('Registration Complete');
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
// verify grid data
final expected = [
original[0],
original[1],
original[2],
original[6],
original[3],
original[4],
original[7],
original[8],
original[9],
original[5],
original[10],
original[11],
original[12],
];
actual = tester.getGridRows();
expect(actual, orderedEquals(expected));
});
});
}
@@ -0,0 +1,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 row test:', () {
testWidgets('create 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('create 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('create 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('create 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));
});
testWidgets('delete row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.pumpAndSettle();
await tester.tapDeleteOnRowMenu();
await tester.pumpAndSettle();
// 3 initial rows - 1 deleted
tester.assertNumberOfRowsInGridPage(2);
});
});
});
}
@@ -0,0 +1,13 @@
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:flutter_test/flutter_test.dart';
extension GridTestExtensions on WidgetTester {
List<RowId> getGridRows() {
final databaseController =
widget<GridPage>(find.byType(GridPage)).databaseController;
return [
...databaseController.rowCache.rowInfos.map((e) => e.rowId),
];
}
}
@@ -15,7 +15,7 @@ import 'package:integration_test/integration_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/editor_test_operations.dart';
import '../../shared/document_test_operations.dart';
import '../../shared/expectation.dart';
import '../../shared/keyboard.dart';
@@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart';
import 'notifications_settings_test.dart' as notifications_settings_test;
import 'settings_billing_test.dart' as settings_billing_test;
import 'shortcuts_settings_test.dart' as shortcuts_settings_test;
import 'sign_in_page_settings_test.dart' as sign_in_page_settings_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -10,4 +11,5 @@ void main() {
notifications_settings_test.main();
settings_billing_test.main();
shortcuts_settings_test.main();
sign_in_page_settings_test.main();
}
@@ -1,11 +1,10 @@
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/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.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';
@@ -16,8 +15,8 @@ import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('shortcuts test', () {
testWidgets('can change and overwrite shortcut', (tester) async {
group('shortcuts:', () {
testWidgets('change and overwrite shortcut', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -29,7 +28,11 @@ void main() {
LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr();
// Input "Delete" into the search field
await tester.enterText(find.byType(TextField), backspaceCmd);
final inputField = find.descendant(
of: find.byType(SettingsShortcutsView),
matching: find.byType(TextField),
);
await tester.enterText(inputField, backspaceCmd);
await tester.pumpAndSettle();
await tester.hoverOnWidget(
@@ -0,0 +1,103 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:toastification/toastification.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Finder findServerType(AuthenticatorType type) {
return find
.descendant(
of: find.byType(SettingsServerDropdownMenu),
matching: find.findTextInFlowyText(
type.label,
),
)
.last;
}
group('sign-in page settings: ', () {
testWidgets('change server type', (tester) async {
await tester.initializeAppFlowy();
// reset the app to the default state
await useAppFlowyBetaCloudWithURL(
kAppflowyCloudUrl,
AuthenticatorType.appflowyCloud,
);
// open the settings page
final settingsButton = find.byType(DesktopSignInSettingsButton);
await tester.tapButton(settingsButton);
expect(find.byType(SimpleSettingsDialog), findsOneWidget);
// the default type should be appflowy cloud
final appflowyCloudType = findServerType(AuthenticatorType.appflowyCloud);
expect(appflowyCloudType, findsOneWidget);
// change the server type to self-host
await tester.tapButton(appflowyCloudType);
final selfhostedButton = findServerType(
AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapButton(selfhostedButton);
// update server url
const serverUrl = 'https://test.appflowy.cloud';
await tester.enterText(
find.byKey(kSelfHostedTextInputFieldKey),
serverUrl,
);
await tester.pumpAndSettle();
await tester.tapButton(
find.findTextInFlowyText(LocaleKeys.button_save.tr()),
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// open settings page to check the result
await tester.tapButton(settingsButton);
// check the server type
expect(
findServerType(AuthenticatorType.appflowyCloudSelfHost),
findsOneWidget,
);
// check the server url
expect(
find.text(serverUrl),
findsOneWidget,
);
// reset to appflowy cloud
await tester.tapButton(
findServerType(AuthenticatorType.appflowyCloudSelfHost),
);
// change the server type to appflowy cloud
await tester.tapButton(
findServerType(AuthenticatorType.appflowyCloud),
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// check the server type
await tester.tapButton(settingsButton);
expect(
findServerType(AuthenticatorType.appflowyCloud),
findsOneWidget,
);
});
});
}
@@ -1,4 +1,3 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
@@ -8,7 +7,6 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -17,7 +15,7 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('sidebar test', () {
group('sidebar:', () {
testWidgets('create a new page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -26,9 +24,7 @@ void main() {
await tester.tapNewPageButton();
// expect to see a new document
tester.expectToSeePageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
tester.expectToSeePageName('');
// and with one paragraph block
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
});
@@ -0,0 +1,122 @@
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:integration_test/integration_test.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Zoom in/out:', () {
Future<void> resetAppFlowyScaleFactor(
WindowSizeManager windowSizeManager,
) async {
appflowyScaleFactor = 1.0;
await windowSizeManager.setScaleFactor(1.0);
}
testWidgets('Zoom in', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
double currentScaleFactor = 1.0;
// this value can't be defined in the setUp method, because the windowSizeManager is not initialized yet.
final windowSizeManager = WindowSizeManager();
await resetAppFlowyScaleFactor(windowSizeManager);
// zoom in 2 times
for (final keycode in zoomInKeyCodes) {
if (UniversalPlatform.isLinux &&
keycode.logicalKey == LogicalKeyboardKey.add) {
// Key LogicalKeyboardKey#79c9b(keyId: "0x0000002b", keyLabel: "+", debugName: "Add") not found in
// linux keyCode map
continue;
}
// test each keycode 2 times
for (var i = 0; i < 2; i++) {
await tester.simulateKeyEvent(
keycode.logicalKey,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
// Register the physical key for the "Add" key, otherwise the test will fail and throw an error:
// Physical key for LogicalKeyboardKey#79c9b(keyId: "0x0000002b", keyLabel: "+", debugName: "Add")
// not found in known physical keys
physicalKey: keycode.logicalKey == LogicalKeyboardKey.add
? PhysicalKeyboardKey.equal
: null,
);
await tester.pumpAndSettle();
currentScaleFactor += 0.1;
final scaleFactor = await windowSizeManager.getScaleFactor();
expect(currentScaleFactor, appflowyScaleFactor);
expect(currentScaleFactor, scaleFactor);
}
}
});
testWidgets('Reset zoom', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final windowSizeManager = WindowSizeManager();
for (final keycode in resetZoomKeyCodes) {
await tester.simulateKeyEvent(
keycode.logicalKey,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
final scaleFactor = await windowSizeManager.getScaleFactor();
expect(1.0, appflowyScaleFactor);
expect(1.0, scaleFactor);
}
});
testWidgets('Zoom out', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
double currentScaleFactor = 1.0;
final windowSizeManager = WindowSizeManager();
await resetAppFlowyScaleFactor(windowSizeManager);
// zoom out 2 times
for (final keycode in zoomOutKeyCodes) {
if (UniversalPlatform.isLinux &&
keycode.logicalKey == LogicalKeyboardKey.numpadSubtract) {
// Key LogicalKeyboardKey#2c39f(keyId: "0x20000022d", keyLabel: "Numpad Subtract", debugName: "Numpad
// Subtract") not found in linux keyCode map
continue;
}
// test each keycode 2 times
for (var i = 0; i < 2; i++) {
await tester.simulateKeyEvent(
keycode.logicalKey,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
currentScaleFactor -= 0.1;
final scaleFactor = await windowSizeManager.getScaleFactor();
expect(currentScaleFactor, appflowyScaleFactor);
expect(currentScaleFactor, scaleFactor);
}
}
});
});
}
@@ -6,8 +6,8 @@ import 'desktop/database/database_field_settings_test.dart'
as database_field_settings_test;
import 'desktop/database/database_field_test.dart' as database_field_test;
import 'desktop/database/database_filter_test.dart' as database_filter_test;
import 'desktop/database/database_media_test.dart' as database_media_test;
import 'desktop/database/database_row_page_test.dart' as database_row_page_test;
import 'desktop/database/database_row_test.dart' as database_row_test;
import 'desktop/database/database_setting_test.dart' as database_setting_test;
import 'desktop/database/database_share_test.dart' as database_share_test;
import 'desktop/database/database_sort_test.dart' as database_sort_test;
@@ -29,12 +29,12 @@ Future<void> runIntegration2OnDesktop() async {
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_row_test.main();
database_setting_test.main();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
database_media_test.main();
// DON'T add more tests here. This is the second test runner for desktop.
}
@@ -1,6 +1,19 @@
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/document/document_title_test.dart' as document_title_test;
import 'desktop/document/document_app_lifecycle_test.dart'
as document_app_lifecycle_test;
import 'desktop/document/document_sub_page_test.dart' as document_sub_page_test;
import 'desktop/grid/grid_filter_and_sort_test.dart'
as grid_filter_and_sort_test_runner;
import 'desktop/grid/grid_reopen_test.dart' as grid_reopen_test_runner;
import 'desktop/grid/grid_reorder_row_test.dart'
as grid_reorder_row_test_runner;
import 'desktop/grid/grid_edit_row_test.dart' as grid_edit_row_test_runner;
import 'desktop/grid/grid_row_test.dart' as grid_create_row_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
@@ -9,6 +22,7 @@ import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
import 'desktop/uncategorized/import_files_test.dart' as import_files_test;
import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/uncategorized/zoom_in_out_test.dart' as zoom_in_out_test;
Future<void> main() async {
await runIntegration3OnDesktop();
@@ -30,4 +44,14 @@ 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();
grid_edit_row_test_runner.main();
zoom_in_out_test.main();
document_title_test.main();
document_sub_page_test.main();
document_app_lifecycle_test.main();
}
@@ -0,0 +1,5 @@
import 'workspace/workspace_operations_test.dart' as workspace_operations_test;
Future<void> main() async {
workspace_operations_test.main();
}
@@ -0,0 +1,67 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.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/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('workspace operations:', () {
testWidgets('create a new workspace', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// click the create a new workspace button
await tester.tapButton(find.text(Constants.defaultWorkspaceName));
await tester.tapButton(find.text(LocaleKeys.workspace_create.tr()));
// input the new workspace name
final inputField = find.byType(TextFormField);
const newWorkspaceName = 'AppFlowy';
await tester.enterText(inputField, newWorkspaceName);
await tester.pumpAndSettle();
// wait for the workspace to be created
await tester.pumpUntilFound(
find.text(LocaleKeys.workspace_createSuccess.tr()),
);
// expect to see the new workspace
expect(find.text(newWorkspaceName), findsOneWidget);
});
});
}
@@ -1,5 +1,6 @@
import 'package:integration_test/integration_test.dart';
import 'mobile/document/page_style_test.dart' as page_style_test;
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
@@ -7,4 +8,5 @@ Future<void> runIntegrationOnMobile() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main();
create_new_page_test.main();
page_style_test.main();
}
@@ -10,9 +10,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
Future<void> tapGoogleLoginInButton({bool pumpAndSettle = true}) async {
await tapButton(
find.byKey(signInWithGoogleButtonKey),
pumpAndSettle: pumpAndSettle,
);
}
@@ -174,7 +174,7 @@ extension AppFlowyTestBase on WidgetTester {
await this.pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 5),
const Duration(seconds: 15),
);
}
}
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -16,12 +17,16 @@ import 'package:appflowy/shared/feature_flags.dart';
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/footer/sidebar_footer.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';
@@ -58,6 +63,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,
@@ -227,6 +240,10 @@ extension CommonOperations on WidgetTester {
await tapOKButton();
}
Future<void> tapTrashButton() async {
await tap(find.byType(SidebarTrashButton));
}
Future<void> tapOKButton() async {
final okButton = find.byWidgetPredicate(
(widget) =>
@@ -236,6 +253,20 @@ 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.descendant(
of: page,
matching: find.byType(ViewItemDefaultLeftIcon),
);
await tapButton(expandButton.first);
}
/// Tap the restore button.
///
/// the restore button will show after the current page is deleted.
@@ -293,10 +324,14 @@ extension CommonOperations on WidgetTester {
}
await pumpAndSettle();
final defaultPageName = layout == ViewLayoutPB.Document
? '' // the document name is empty by default
: LocaleKeys.menuAppHeader_defaultNewPageName.tr();
// hover on it and change it's name
if (name != null) {
await hoverOnPageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
defaultPageName,
layout: layout,
onHover: () async {
await renamePage(name);
@@ -310,13 +345,87 @@ extension CommonOperations on WidgetTester {
if (openAfterCreated) {
await openPage(
// if the name is null, use the default name
name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
name ?? defaultPageName,
layout: layout,
);
await pumpAndSettle();
}
}
/// 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(
'',
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);
}
/// Click the + button in the space header
Future<void> clickSpaceHeader() async {
await tapButton(find.byType(SidebarSpaceHeader));
}
Future<void> openSpace(String spaceName) async {
final space = find.descendant(
of: find.byType(SidebarSpaceMenuItem),
matching: find.text(spaceName),
);
await tapButton(space);
}
/// Create a new page on the top level
Future<void> createNewPage({
ViewLayoutPB layout = ViewLayoutPB.Document,
bool openAfterCreated = true,
@@ -330,6 +439,7 @@ extension CommonOperations on WidgetTester {
bool isShiftPressed = false,
bool isAltPressed = false,
bool isMetaPressed = false,
PhysicalKeyboardKey? physicalKey,
}) async {
if (isControlPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.control);
@@ -343,8 +453,14 @@ extension CommonOperations on WidgetTester {
if (isMetaPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.meta);
}
await simulateKeyDownEvent(key);
await simulateKeyUpEvent(key);
await simulateKeyDownEvent(
key,
physicalKey: physicalKey,
);
await simulateKeyUpEvent(
key,
physicalKey: physicalKey,
);
if (isControlPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.control);
}
@@ -540,7 +656,11 @@ extension CommonOperations on WidgetTester {
expect(createWorkspaceDialog, findsOneWidget);
// input the workspace name
await enterText(find.byType(TextField), name);
final workspaceNameInput = find.descendant(
of: createWorkspaceDialog,
matching: find.byType(TextField),
);
await enterText(workspaceNameInput, name);
await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false);
await pump(const Duration(seconds: 5));
@@ -572,8 +692,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 +700,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 +717,7 @@ extension CommonOperations on WidgetTester {
of: find.byType(ListView),
matching: find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewActionType.delete,
widget is ViewAction && widget.type == ViewMoreActionType.delete,
),
);
await tap(button);
@@ -0,0 +1,8 @@
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';
static const defaultWorkspaceName = 'My Workspace';
}
@@ -1,9 +1,8 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:archive/archive_io.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -60,7 +59,7 @@ class TestWorkspaceService {
final inputStream =
InputFileStream(await workspace.zip.then((value) => value.path));
final archive = ZipDecoder().decodeBuffer(inputStream);
extractArchiveToDisk(
await extractArchiveToDisk(
archive,
await TestWorkspace._parent.then((value) => value.path),
);
@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -20,13 +23,14 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
@@ -95,8 +99,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 +113,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 {
@@ -149,6 +151,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(cell, findsOneWidget);
await enterText(cell, input);
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
}
@@ -244,10 +247,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
Future<void> assertMultiSelectOption({
void assertMultiSelectOption({
required int rowIndex,
required List<String> contents,
}) async {
}) {
final findCell = cellFinder(rowIndex, FieldType.MultiSelect);
for (final content in contents) {
if (content.isNotEmpty) {
@@ -415,10 +418,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(option);
}
Future<void> findSelectOptionWithNameInGrid({
void findSelectOptionWithNameInGrid({
required int rowIndex,
required String name,
}) async {
}) {
final findRow = find.byType(GridRow);
final option = find.byWidgetPredicate(
(widget) =>
@@ -430,10 +433,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(cell, findsOneWidget);
}
Future<void> assertNumberOfSelectedOptionsInGrid({
void assertNumberOfSelectedOptionsInGrid({
required int rowIndex,
required Matcher matcher,
}) async {
}) {
final findRow = find.byType(GridRow);
final options = find.byWidgetPredicate(
@@ -571,8 +574,19 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
/// Used to open the add cover popover, by pressing on "Add cover"-button.
///
/// Should call [hoverRowBanner] first.
///
Future<void> tapAddCoverButton() async {
await tapButtonWithName(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
await pumpAndSettle();
}
Future<void> openEmojiPicker() async =>
tapButton(find.byType(AddEmojiButton));
tapButton(find.text(LocaleKeys.document_plugins_cover_addIcon.tr()));
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(EditableDateCell);
@@ -670,6 +684,53 @@ extension AppFlowyDatabaseTest on WidgetTester {
await dismissFieldEditor();
}
Future<void> changeFieldIcon(String icon) async {
await tapButton(find.byType(FieldEditIconButton));
if (icon.isEmpty) {
final button = find.descendant(
of: find.byType(FlowyIconEmojiPicker),
matching: find.text(
LocaleKeys.button_remove.tr(),
),
);
await tapButton(button);
} else {
final svgContent = kIconGroups?.findSvgContent(icon);
await tapButton(
find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svgString == svgContent,
),
);
}
}
void assertFieldSvg(String name, FieldType fieldType) {
final svgFinder = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == fieldType.svgData,
);
final fieldButton = find.byWidgetPredicate(
(widget) => widget is FieldCellButton && widget.field.name == name,
);
expect(
find.descendant(of: fieldButton, matching: svgFinder),
findsOneWidget,
);
}
void assertFieldCustomSvg(String name, String svg) {
final svgContent = kIconGroups?.findSvgContent(svg);
final svgFinder = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svgString == svgContent,
);
final fieldButton = find.byWidgetPredicate(
(widget) => widget is FieldCellButton && widget.field.name == name,
);
expect(
find.descendant(of: fieldButton, matching: svgFinder),
findsOneWidget,
);
}
Future<void> changeCalculateAtIndex(int index, CalculationType type) async {
await tap(find.byType(CalculateCell).at(index));
await pumpAndSettle();
@@ -787,7 +848,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 +933,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,
@@ -918,7 +1009,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> tapCreateFilterByFieldType(FieldType type, String title) async {
final findFilter = find.byWidgetPredicate(
(widget) =>
widget is GridFilterPropertyCell &&
widget is FilterableFieldButton &&
widget.fieldInfo.fieldType == type &&
widget.fieldInfo.name == title,
);
@@ -926,8 +1017,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> tapFilterButtonInGrid(String name) async {
final findFilter = find.byType(FilterMenuItem);
final button = find.descendant(of: findFilter, matching: find.text(name));
final button = find.byWidgetPredicate(
(widget) => widget is ChoiceChipButton && widget.fieldInfo.name == name,
);
await tapButton(button);
}
@@ -965,12 +1057,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// Must call [tapSortMenuInSettingBar] first.
Future<void> tapSortButtonByName(String name) async {
final findSortItem = find.byWidgetPredicate(
(widget) =>
widget is DatabaseSortItem && widget.sortInfo.fieldInfo.name == name,
Future<void> tapEditSortConditionButtonByFieldName(String name) async {
final sortItem = find.descendant(
of: find.ancestor(
of: find.text(name),
matching: find.byType(DatabaseSortItem),
),
matching: find.byType(SortConditionButton),
);
await tapButton(findSortItem);
await tapButton(sortItem);
}
/// Must call [tapSortMenuInSettingBar] first.
@@ -978,18 +1073,26 @@ extension AppFlowyDatabaseTest on WidgetTester {
(FieldType, String) from,
(FieldType, String) to,
) async {
final fromSortItem = find.byWidgetPredicate(
(widget) =>
widget is DatabaseSortItem &&
widget.sortInfo.fieldInfo.fieldType == from.$1 &&
widget.sortInfo.fieldInfo.name == from.$2,
final fromSortItem = find.ancestor(
of: find.text(from.$2),
matching: find.byType(DatabaseSortItem),
);
final toSortItem = find.byWidgetPredicate(
(widget) =>
widget is DatabaseSortItem &&
widget.sortInfo.fieldInfo.fieldType == to.$1 &&
widget.sortInfo.fieldInfo.name == to.$2,
final toSortItem = find.ancestor(
of: find.text(to.$2),
matching: find.byType(DatabaseSortItem),
);
// final fromSortItem = find.byWidgetPredicate(
// (widget) =>
// widget is DatabaseSortItem &&
// widget.sort.fieldInfo.fieldType == from.$1 &&
// widget.sort.fieldInfo.name == from.$2,
// );
// final toSortItem = find.byWidgetPredicate(
// (widget) =>
// widget is DatabaseSortItem &&
// widget.sort.fieldInfo.fieldType == to.$1 &&
// widget.sort.fieldInfo.name == to.$2,
// );
final dragElement = find.descendant(
of: fromSortItem,
matching: find.byType(ReorderableDragStartListener),
@@ -998,16 +1101,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 200));
}
/// Must call [tapSortButtonByName] first.
/// Must call [tapEditSortConditionButtonByFieldName] first.
Future<void> tapSortByDescending() async {
await tapButton(
find.descendant(
of: find.byType(OrderPannelItem),
matching: find.byWidgetPredicate(
(widget) =>
widget is FlowyText &&
widget.text == LocaleKeys.grid_sort_descending.tr(),
),
find.byWidgetPredicate(
(widget) =>
widget is OrderPanelItem &&
widget.condition == SortConditionPB.Descending,
),
);
await sendKeyEvent(LogicalKeyboardKey.escape);
@@ -1015,7 +1115,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// Must call [tapSortMenuInSettingBar] first.
Future<void> tapAllSortButton() async {
Future<void> tapDeleteAllSortsButton() async {
await tapButton(find.byType(DeleteAllSortsButton));
}
@@ -1057,6 +1157,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(ChecklistFilterConditionList));
}
Future<void> tapDateFilterButtonInGrid() async {
await tapButton(find.byType(DateFilterConditionList));
}
/// The [SelectOptionFilterList] must show up first.
Future<void> tapOptionFilterWithName(String name) async {
final findCell = find.descendant(
@@ -1090,6 +1194,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> tapDateFilterCondition(DateTimeFilterCondition condition) async {
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(condition.filterName),
);
await tapButton(button);
}
/// Should call [tapDatabaseSettingButton] first.
Future<void> tapViewPropertiesButton() async {
final findSettingItem = find.byType(DatabaseSettingsList);
@@ -1252,7 +1365,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 +1381,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
);
await enterText(textField, title);
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle(const Duration(milliseconds: 300));
}
@@ -1,21 +1,24 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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/cover_title.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -33,11 +36,19 @@ 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);
index = index.clamp(0, textBlocks.evaluate().length - 1);
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
final center = tester.getCenter(textBlocks.at(index));
final right = tester.getTopRight(textBlocks.at(index));
final centerRight = Offset(right.dx, center.dy);
await tester.tapAt(centerRight);
await tester.pumpAndSettle();
}
@@ -205,7 +216,7 @@ class EditorOperations {
}
/// Update the editor's selection
Future<void> updateSelection(Selection selection) async {
Future<void> updateSelection(Selection? selection) async {
final editorState = getCurrentEditorState();
unawaited(
editorState.updateSelectionWithReason(
@@ -266,4 +277,78 @@ 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);
}
Finder findDocumentTitle(String title) {
return find.descendant(
of: find.byType(CoverTitle),
matching: find.byWidgetPredicate(
(widget) {
if (widget is! TextField) {
return false;
}
if (widget.controller?.text == title) {
return true;
}
if (title.isEmpty) {
return widget.controller?.text.isEmpty ?? false;
}
return false;
},
),
);
}
}
@@ -1,8 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
@@ -11,11 +12,11 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_platform/universal_platform.dart';
import 'util.dart';
@@ -25,9 +26,15 @@ const String gettingStarted = 'Getting started';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
Future<void> expectToSeeHomePageWithGetStartedPage() async {
final finder = find.byType(HomeStack);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
if (UniversalPlatform.isDesktopOrWeb) {
final finder = find.byType(HomeStack);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
} else if (UniversalPlatform.isMobile) {
final finder = find.byType(MobileHomePage);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
}
final docFinder = find.textContaining(gettingStarted);
await pumpUntilFound(docFinder);
@@ -184,7 +191,7 @@ extension Expectation on WidgetTester {
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
if (PlatformExtension.isDesktop) {
if (UniversalPlatform.isDesktop) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
@@ -1,9 +1,9 @@
export 'auth_operation.dart';
export 'base.dart';
export 'common_operations.dart';
export 'settings.dart';
export 'data.dart';
export 'document_test_operations.dart';
export 'expectation.dart';
export 'editor_test_operations.dart';
export 'mock/mock_url_launcher.dart';
export 'ime.dart';
export 'auth_operation.dart';
export 'mock/mock_url_launcher.dart';
export 'settings.dart';
+6 -6
View File
@@ -170,7 +170,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 +180,25 @@ 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_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -35,6 +35,7 @@ class KVKeys {
'kDocumentAppearanceCursorColor';
static const String kDocumentAppearanceSelectionColor =
'kDocumentAppearanceSelectionColor';
static const String kDocumentAppearanceWidth = 'kDocumentAppearanceWidth';
/// The key for saving the expanded views
///
@@ -56,8 +57,6 @@ class KVKeys {
static const String kCloudType = 'kCloudType';
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
static const String kSupabaseURL = 'kSupabaseURL';
static const String kSupabaseAnonKey = 'kSupabaseAnonKey';
/// The key for saving the text scale factor.
///
+6 -24
View File
@@ -14,8 +14,12 @@ import 'package:appflowy_backend/log.dart';
/// [ty] - The type of cloud to be set. It must be one of the values from
/// [AuthenticatorType] enum. The corresponding integer value of the enum is stored:
/// - `CloudType.local` is stored as "0".
/// - `CloudType.supabase` is stored as "1".
/// - `CloudType.appflowyCloud` is stored as "2".
///
/// The gap between [AuthenticatorType.local] and [AuthenticatorType.appflowyCloud] is
/// due to previously supporting Supabase, this has been deprecated since and removed.
/// To not cause conflicts with older clients, we keep the gap.
///
Future<void> _setAuthenticatorType(AuthenticatorType ty) async {
switch (ty) {
case AuthenticatorType.local:
@@ -84,7 +88,7 @@ Future<AuthenticatorType> getAuthenticatorType() async {
/// A boolean value indicating whether authentication is enabled. It returns
/// `true` if the application is in release or develop mode, and the cloud type
/// is not set to `CloudType.local`. Additionally, it checks if either the
/// AppFlowy Cloud or Supabase configuration is valid.
/// AppFlowy Cloud configuration is valid.
/// Returns `false` otherwise.
bool get isAuthEnabled {
final env = getIt<AppFlowyCloudSharedEnv>();
@@ -286,25 +290,3 @@ Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
Future<String> _getAppFlowyCloudGotrueUrl(String baseURL) async {
return "$baseURL/gotrue";
}
Future<void> setSupabaseServer(
String? url,
String? anonKey,
) async {
assert(
(url != null && anonKey != null) || (url == null && anonKey == null),
"Either both Supabase URL and anon key must be set, or both should be unset",
);
if (url == null) {
await getIt<KeyValueStorage>().remove(KVKeys.kSupabaseURL);
} else {
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseURL, url);
}
if (anonKey == null) {
await getIt<KeyValueStorage>().remove(KVKeys.kSupabaseAnonKey);
} else {
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, anonKey);
}
}
@@ -391,17 +391,23 @@ class _AFDropdownMenuState<T> extends State<AFDropdownMenu<T>> {
);
}
// Remove the code here, it will throw a FlutterError
// Unless we upgrade to Flutter 3.24 https://github.com/flutter/flutter/issues/146764
void scrollToHighlight() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final BuildContext? highlightContext =
buttonItemKeys[currentHighlight!].currentContext;
if (highlightContext != null) {
Scrollable.ensureVisible(highlightContext);
}
},
debugLabel: 'DropdownMenu.scrollToHighlight',
);
// WidgetsBinding.instance.addPostFrameCallback(
// (_) {
// // try {
// final BuildContext? highlightContext =
// buttonItemKeys[currentHighlight!].currentContext;
// if (highlightContext != null) {
// Scrollable.ensureVisible(highlightContext);
// }
// } catch (_) {
// return;
// }
// },
// debugLabel: 'DropdownMenu.scrollToHighlight',
// );
}
double? getWidth(GlobalKey key) {
@@ -49,7 +49,13 @@ class BlockActionBottomSheet extends StatelessWidget {
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_duplicate_s),
leftIcon: const Padding(
padding: EdgeInsets.all(2),
child: FlowySvg(
FlowySvgs.copy_s,
size: Size.square(16),
),
),
onTap: () => onAction(BlockActionBottomSheetType.duplicate),
),
@@ -59,7 +65,8 @@ class BlockActionBottomSheet extends StatelessWidget {
showTopBorder: false,
text: LocaleKeys.button_delete.tr(),
leftIcon: FlowySvg(
FlowySvgs.m_delete_s,
FlowySvgs.trash_s,
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
textColor: Theme.of(context).colorScheme.error,
@@ -48,12 +48,14 @@ Future<T?> showMobileBottomSheet<T>(
double? elevation,
bool showDoneButton = false,
bool enableDraggableScrollable = false,
bool enableScrollable = false,
// this field is only used if showDragHandle is true
Widget Function(BuildContext, ScrollController)? scrollableWidgetBuilder,
// only used when enableDraggableScrollable is true
double minChildSize = 0.5,
double maxChildSize = 0.8,
double initialChildSize = 0.51,
double bottomSheetPadding = 0,
}) async {
assert(
showHeader ||
@@ -151,6 +153,19 @@ Future<T?> showMobileBottomSheet<T>(
);
},
);
} else if (enableScrollable) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
...children,
Flexible(
child: SingleChildScrollView(
child: child,
),
),
VSpace(bottomSheetPadding),
],
);
}
// ----- content area -----
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/board.dart';
@@ -5,11 +7,11 @@ import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_h
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -17,7 +19,6 @@ import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -235,29 +236,33 @@ class _BoardContentState extends State<_BoardContent> {
key: ValueKey(groupItemId),
margin: cardMargin,
decoration: _makeBoxDecoration(context),
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
child: BlocProvider.value(
value: boardBloc,
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
),
userProfile: boardBloc.userProfile,
),
),
);
@@ -3,7 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -21,6 +21,10 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dar
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -146,6 +150,58 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
text: LocaleKeys.button_duplicate.tr(),
),
const Divider(height: 8.5, thickness: 0.5),
MobileQuickActionButton(
onTap: () => showMobileBottomSheet(
context,
title: LocaleKeys.grid_media_addFileMobile.tr(),
showHeader: true,
showCloseButton: true,
showDragHandle: true,
builder: (dialogContext) => Container(
margin: const EdgeInsets.only(top: 12),
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: FileUploadMenu(
onInsertLocalFile: (files) async {
context
..pop()
..pop();
if (_bloc.state.currentRowId == null) {
return;
}
await insertLocalFiles(
context,
files,
userProfile: _bloc.userProfile,
documentId: _bloc.state.currentRowId!,
onUploadSuccess: (file, path, isLocalMode) {
_bloc.add(
MobileRowDetailEvent.addCover(
RowCoverPB(
data: path,
uploadType: isLocalMode
? FileUploadTypePB.LocalFile
: FileUploadTypePB.CloudFile,
coverType: CoverTypePB.FileCover,
),
),
);
},
);
},
onInsertNetworkFile: (url) async =>
_onInsertNetworkFile(url, context),
),
),
),
icon: FlowySvgs.add_cover_s,
text: 'Add cover',
),
const Divider(height: 8.5, thickness: 0.5),
MobileQuickActionButton(
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
text: LocaleKeys.button_delete.tr(),
@@ -178,6 +234,38 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
gravity: ToastGravity.BOTTOM,
);
}
Future<void> _onInsertNetworkFile(
String url,
BuildContext context,
) async {
context
..pop()
..pop();
if (url.isEmpty) return;
final uri = Uri.tryParse(url);
if (uri == null) {
return;
}
String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : "";
if (name.isEmpty && uri.pathSegments.length > 1) {
name = uri.pathSegments[uri.pathSegments.length - 2];
} else if (name.isEmpty) {
name = uri.host;
}
_bloc.add(
MobileRowDetailEvent.addCover(
RowCoverPB(
data: url,
uploadType: FileUploadTypePB.NetworkFile,
coverType: CoverTypePB.FileCover,
),
),
);
}
}
class RowDetailFab extends StatelessWidget {
@@ -322,91 +410,129 @@ class MobileRowDetailPageContentState
rowController: rowController,
),
child: BlocBuilder<RowDetailBloc, RowDetailState>(
builder: (context, rowDetailState) {
return Column(
children: [
BlocProvider<RowBannerBloc>(
create: (context) => RowBannerBloc(
viewId: viewId,
fieldController: fieldController,
rowMeta: rowController.rowMeta,
)..add(const RowBannerEvent.initial()),
child: BlocConsumer<RowBannerBloc, RowBannerState>(
listener: (context, state) {
if (state.primaryField == null) {
return;
}
primaryFieldId.value = state.primaryField!.id;
},
builder: (context, state) {
if (state.primaryField == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: cellBuilder.buildCustom(
CellContext(
rowId: rowController.rowId,
fieldId: state.primaryField!.id,
),
skinMap: EditableCellSkinMap(
textSkin: _TitleSkin(),
),
builder: (context, rowDetailState) => Column(
children: [
if (rowDetailState.rowMeta.cover.data.isNotEmpty) ...[
GestureDetector(
onTap: () => showMobileBottomSheet(
context,
backgroundColor: AFThemeExtension.of(context).background,
showDragHandle: true,
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
MobileQuickActionButton(
onTap: () {
context
..pop()
..read<RowDetailBloc>()
.add(const RowDetailEvent.removeCover());
},
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
icon: FlowySvgs.trash_s,
iconColor: Theme.of(context).colorScheme.error,
),
);
},
],
),
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 9, bottom: 100),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: MobileRowPropertyList(
databaseController: widget.databaseController,
cellBuilder: cellBuilder,
),
child: SizedBox(
height: 200,
width: double.infinity,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
),
Padding(
padding: const EdgeInsets.fromLTRB(6, 6, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (rowDetailState.numHiddenFields != 0) ...[
const ToggleHiddenFieldsVisibilityButton(),
],
const VSpace(8.0),
ValueListenableBuilder(
valueListenable: primaryFieldId,
builder: (context, primaryFieldId, child) {
if (primaryFieldId.isEmpty) {
return const SizedBox.shrink();
}
return OpenRowPageButton(
databaseController: widget.databaseController,
cellContext: CellContext(
rowId: rowController.rowId,
fieldId: primaryFieldId,
),
documentId: rowController.rowMeta.documentId,
);
},
),
MobileRowDetailCreateFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
child: AFImage(
url: rowDetailState.rowMeta.cover.data,
uploadType: widget.rowMeta.cover.uploadType,
userProfile:
context.read<MobileRowDetailBloc>().userProfile,
),
],
),
),
),
],
);
},
BlocProvider<RowBannerBloc>(
create: (context) => RowBannerBloc(
viewId: viewId,
fieldController: fieldController,
rowMeta: rowController.rowMeta,
)..add(const RowBannerEvent.initial()),
child: BlocConsumer<RowBannerBloc, RowBannerState>(
listener: (context, state) {
if (state.primaryField == null) {
return;
}
primaryFieldId.value = state.primaryField!.id;
},
builder: (context, state) {
if (state.primaryField == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: cellBuilder.buildCustom(
CellContext(
rowId: rowController.rowId,
fieldId: state.primaryField!.id,
),
skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),
),
);
},
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 9, bottom: 100),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: MobileRowPropertyList(
databaseController: widget.databaseController,
cellBuilder: cellBuilder,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(6, 6, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (rowDetailState.numHiddenFields != 0) ...[
const ToggleHiddenFieldsVisibilityButton(),
],
const VSpace(8.0),
ValueListenableBuilder(
valueListenable: primaryFieldId,
builder: (context, primaryFieldId, child) {
if (primaryFieldId.isEmpty) {
return const SizedBox.shrink();
}
return OpenRowPageButton(
databaseController: widget.databaseController,
cellContext: CellContext(
rowId: rowController.rowId,
fieldId: primaryFieldId,
),
documentId: rowController.rowMeta.documentId,
);
},
),
MobileRowDetailCreateFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
),
],
),
),
],
),
),
);
}
@@ -429,7 +555,9 @@ class _TitleSkin extends IEditableTextCellSkin {
fontSize: 23,
fontWeight: FontWeight.w500,
),
onChanged: (text) => bloc.add(TextCellEvent.updateText(text)),
onEditingComplete: () {
bloc.add(TextCellEvent.updateText(textEditingController.text));
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 9),
border: InputBorder.none,
@@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
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/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileRowPropertyList extends StatelessWidget {
@@ -77,10 +75,8 @@ class _PropertyCellState extends State<_PropertyCell> {
children: [
Row(
children: [
FlowySvg(
fieldInfo.fieldType.svgData,
color: Theme.of(context).hintColor,
size: const Size.square(16),
FieldIcon(
fieldInfo: fieldInfo,
),
const HSpace(6),
Expanded(
@@ -11,13 +11,13 @@ class OptionTextField extends StatelessWidget {
const OptionTextField({
super.key,
required this.controller,
required this.type,
required this.fieldType,
required this.onTextChanged,
required this.onFieldTypeChanged,
});
final TextEditingController controller;
final FieldType type;
final FieldType fieldType;
final void Function(String value) onTextChanged;
final void Function(FieldType value) onFieldTypeChanged;
@@ -43,12 +43,12 @@ class OptionTextField extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).brightness == Brightness.light
? type.mobileIconBackgroundColor
: type.mobileIconBackgroundColorDark,
? fieldType.mobileIconBackgroundColor
: fieldType.mobileIconBackgroundColorDark,
),
child: Center(
child: FlowySvg(
type.svgData,
fieldType.svgData,
size: const Size.square(22),
),
),
@@ -90,7 +90,7 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
),
onPressed: () {
final name = state.content;
_openRowPage(context, name);
_openRowPage(context, name ?? "");
},
),
);
@@ -1,16 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
class MobileCardContent extends StatelessWidget {
const MobileCardContent({
@@ -19,57 +14,38 @@ class MobileCardContent extends StatelessWidget {
required this.cellBuilder,
required this.cells,
required this.styleConfiguration,
required this.userProfile,
});
final RowMetaPB rowMeta;
final CardCellBuilder cellBuilder;
final List<CellMeta> cells;
final RowCardStyleConfiguration styleConfiguration;
final UserProfilePB? userProfile;
@override
Widget build(BuildContext context) {
final attachmentCount = rowMeta.attachmentCount.toInt();
return Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...cells.map(
(cellMeta) {
return cellBuilder.build(
cellContext: cellMeta.cellContext(),
styleMap: mobileBoardCardCellStyleMap(context),
hasNotes: !rowMeta.isDocumentEmpty,
);
},
),
if (attachmentCount > 0) ...[
const VSpace(4),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Row(
children: [
const FlowySvg(
FlowySvgs.media_s,
size: Size.square(12),
),
const HSpace(6),
Flexible(
child: FlowyText.regular(
LocaleKeys.grid_media_attachmentsHint
.tr(args: ['$attachmentCount']),
fontSize: 12,
color: AFThemeExtension.of(context).secondaryTextColor,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (rowMeta.cover.data.isNotEmpty) ...[
CardCover(cover: rowMeta.cover, userProfile: userProfile),
],
),
Padding(
padding: styleConfiguration.cardPadding,
child: Column(
children: [
...cells.map(
(cellMeta) => cellBuilder.build(
cellContext: cellMeta.cellContext(),
styleMap: mobileBoardCardCellStyleMap(context),
hasNotes: !rowMeta.isDocumentEmpty,
),
),
],
),
),
],
);
}
}
@@ -37,6 +37,7 @@ class _MobileNewPropertyScreenState extends State<MobileNewPropertyScreen> {
final type = widget.fieldType ?? FieldType.RichText;
optionValues = FieldOptionValues(
type: type,
icon: "",
name: type.i18n,
);
}
@@ -1,10 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -132,9 +131,9 @@ class _FieldButton extends StatelessWidget {
return FlowyOptionTile.checkbox(
text: field.name,
isSelected: isSelected,
leftIcon: FlowySvg(
field.fieldType.svgData,
size: const Size.square(20),
leftIcon: FieldIcon(
fieldInfo: field,
dimension: 20,
),
showTopBorder: showTopBorder,
onTap: () => onSelect(field.id),
@@ -35,6 +35,7 @@ class FieldOptionValues {
FieldOptionValues({
required this.type,
required this.name,
required this.icon,
this.dateFormat,
this.timeFormat,
this.includeTime,
@@ -48,6 +49,7 @@ class FieldOptionValues {
return FieldOptionValues(
type: fieldType,
name: field.name,
icon: field.icon,
numberFormat: fieldType == FieldType.Number
? NumberTypeOptionPB.fromBuffer(buffer).format
: null,
@@ -83,6 +85,7 @@ class FieldOptionValues {
FieldType type;
String name;
String icon;
// FieldType.DateTime
// FieldType.LastEditedTime
@@ -221,7 +224,7 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
const _Divider(),
OptionTextField(
controller: controller,
type: values.type,
fieldType: values.type,
onTextChanged: (value) {
isFieldNameChanged = true;
_updateOptionValues(name: value);
Loaded 100 of 1063 files, more files were not shown because too many files have changed in this diff. Show more