Compare commits

..
335 Commits
Author SHA1 Message Date
Nathan.fooo ca195887e0 chore: bump collab (#6914)
* chore: bump collab

* chore: bump collab
2024-12-03 17:05:40 +08:00
Kilu.He f9a5458b94 feat: support simple table (#6911) 2024-12-03 15:33:51 +08:00
Lucas 244d072a65 fix: pressing enter on a collapsible list toggle adds an additional new line (#6909)
* fix: pressing Enter on a closed toggle list adds an additional new line

* test: pressing Enter on a closed toggle list adds an additional new line

* chore: upgrade appflowy_editor

* chore: rename table align
2024-12-03 13:56:15 +08:00
Lucas d9bc97e012 chore: remove rename dialog logic (#6906) 2024-12-02 20:01:40 +08:00
LucasandRichard Shiue e7491e5182 feat: simple table issues (#6871)
* fix: disable cut command in table cell

* feat: only keep the table cell content when coping text from table

* fix: focus on the first cell after inserting table

* test: focus on the first cell after inserting table

* feat: highlight the cell when editing

* test: highlight the cell when editing

* fix: creating a new row makes a cursor appear for a fraction of a second

* fix: add 4px between scroll bar and add row button

* chore: rename simple table components

* fix: select all in table cell block

* test: select all in table cell block

* feat: disable two-fingers resize in table cell

* feat: includ table when exporting markdown

* test: include table when exporting markdown

* feat: optimize add row button render logic

* chore: optimize hover button render logic

* fix: column button is not clickable

* fix: theme assertion

* feat: improve hovering logic

* fix: selection issue in table

* fix(flutter_desktop): popover conflicts on simple table

* feat: support table markdown import

* test: table cell isEditing test

* test: select all in table test

* fix: popover conflict in table action menu

* test: insert row, column, row/column in table

* test: delete row, column, row/column in table

* test: enable header column and header row in table

* test: duplicate/insert left/right/above/below in table

* chore: duplicate table in optin menu

* fix: integraion test

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2024-12-02 17:50:32 +08:00
promto-c 550b8835c6 chore: update th-TH translations (#6898) 2024-12-02 14:42:40 +08:00
Mathias Mogensen 1851721d9a fix: tabs lr 0.7.6 (#6899)
* fix: tabs improvements launch review

* test: add tests for pin/unpin feature

* fix: failing test + cleanup
2024-12-02 00:52:36 +01:00
Mathias Mogensen 603d65a790 fix: mobile camera android permission (#6900) 2024-12-02 00:49:54 +01:00
Richard Shiue 0cba3f9e3f fix(mobile): lost initial scroll position on empty ai chat page (#6895) 2024-12-01 13:18:20 +08:00
promto-c 81960a7f05 chore(i18n): add th-TH translations (#6891)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-11-30 21:17:30 +08:00
Lucas 62c4a8c541 fix: unable to render code block in ai writer block (#6883)
* fix: unable to render code block in ai writer block

* feat: enter to generate ai result
2024-11-29 18:49:47 +08:00
Richard Shiueandnathan 510d8357ee fix: RwLock race condition during group controller instantiation (#6860)
* chore: code cleanup

* fix: view editor used during initialization

* fix: quick and dirty hack job

* test: add test

* chore: don't create separate mut var

* chore: docs

* fix: uninitialized group controllers

* chore: remove group

* chore: fix test

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-11-28 17:26:13 +08:00
Mathias Mogensen 4c6f6f14f3 fix: add camera permission for iOS (#6872) 2024-11-28 10:17:24 +01:00
Lucas 27af57289f chore: update version to 0.7.6 (#6870) 2024-11-28 14:04:22 +08:00
Lucas 17aa8c9036 feat: simple table redesign (#6831)
* feat: define the simple table block data strucuture

* fix: close keyboard when uploading file

* feat: integrate a simple table

* feat: support selectable mixin for table

* feat: render border for table

* feat: support add column/row

* feat: support add column and row

* feat: support add column and row button

* feat: support table border and cell border

* feat: use context to manage hovering status

* feat: add hover status for cell

* feat: add hover row action

* feat: support row/column hover action

* feat: support table more actions

* feat: support insert left/right/above/below actions

* feat: add reorder button

* feat: clear content in table

* feat: support header column and header row

* chore: refactor the table more actions

* feat: render border in cell

* feat: add align button

* feat: add align menu

* fix: unable to delete last column

* feat: support drag to resize

* feat: support resizing column and set limitation

* fix: unable to resize

* feat: support table cell align

* fix: type error

* feat: support table cell background color

* fix: enable header column/row bug

* feat: support table background color

* feat: support duplicate table without background color

* feat: support duplicate with column color

* feat: update column width

* feat: move insert operations to a new file

* fix: row more action position is not correct

* fix: delete row error

* feat: support highlight the border after selecting a column/row

* feat: support mapping the insertion color/align

* feat: support mapping the duplication color/align

* fix: delete column error

* feat: support duplication map

* feat: support duplicating column with color and align

* feat: support duplicating row with color and align

* chore: optimize the table_map_operations

* feat: support clear content / header / style operations

* chore: remove null value in table attributes

* fix: delete row issues

* fix: delete column issues

* fix: unable to clear content

* feat: support arrow key shortcuts

* feat: support tab shortcuts

* feat: support backspace key shortcuts

* feat: support table block selection

* feat: set text in header column / row to bold

* fix: flutter analyze

* chore: enable debug log in table

* test: simple table insert operation

* test: simple table delete operation

* test: simple table header operation

* test: simple table style operation - column width

* test: simple table style operation - background color and align

* test: simple table content operation - clear content

* test: simple table operations - insertion, deletion and duplication

* test: simple table operations - duplicate with background and align

* fix: hide the delete button if there is only one column or one row

* test: add integration test and i18n

* chore: update translations

* fix: drag visual issue

* fix: exclude the popup menu in table more actions

* feat: only show the add button when hovering on the edge
2024-11-28 13:58:00 +08:00
Mathias Mogensen c910bda534 fix: minor improvements to media interactions (#6867)
* fix: minor improvements to media interactions

* test: remove redundant test
2024-11-28 02:13:06 +01:00
Mathias Mogensen 8d01b96281 feat: pin tabs (#6869) 2024-11-28 01:20:47 +01:00
Mathias Mogensen 55f12d5358 feat: attachment mobile toolbar item (#6868) 2024-11-28 01:20:30 +01:00
Mathias Mogensen 018c146d72 fix: built in database height issue (#6866)
* fix: built in database height issue

* fix: use shrinkWrap to decide height

* fix: pageview for lazy rendering
2024-11-27 15:37:45 +01:00
Lucas 068ac0e992 feat: support markdown syntax in ai writer block (#6864)
* feat: support markdown syntax in ai writer block

* feat: support nested list

* test: add markdown text robot tests

* feat: support in-memory update in ai-writer block

* feat: support markdown in ask ai block

* feat: render ai result with appflowy_editor

* feat: support markdown syntax in ask ai block

* fix: selection after replace
2024-11-27 21:19:24 +08:00
Lucas b036129efb fix: multiple select menu display issue (#6861)
* chore: set the minimum windows size to 640

* feat: optimize text insertion

* fix: unable to scroll down in multiple select options
2024-11-27 09:56:28 +08:00
Richard Shiue b3c8eb151a chore: improve ai chat errors (#6851)
* chore: move margins to layout define

* chore: related question alignment

* chore: adjust vertical spacing around sources

* chore: scroll to bottom animation improvement

* chore: improve ai chat error handling
2024-11-25 18:45:59 +08:00
Lucas e86d584ea7 feat: support skipping in-memory update transaction (#6856)
* feat: support skipping in-memory update transaction

* fix: flutter analyze

* feat: add sentence mode

* test: support skipping in-memory update transaction

* test: add sentence mode

* test: add sentence mode (2)

* chore: set enableDocumentInternalLog to false

* fix: integration test

* fix: integration test
2024-11-25 17:55:15 +08:00
Richard Shiue e0226e54a5 chore: minor ai chat improvements (#6855)
* chore: animation alignment improvement

* fix: missing context in drop zone

* chore: increase minimum padding between chat and edge of screen

* chore: unify padding for ai message loading
2024-11-25 10:43:08 +08:00
Lucas bde1457524 feat: support click to create content inside empty toggle list (#6854)
* feat: support click to create content inside empty toggle list

* test: support click to create content inside empty toggle list

* fix: toggle list rtl issue

* chore: optimize cover title request node logic
2024-11-25 10:39:23 +08:00
Lucas 2ad2a79bd0 fix: macOS error 50 on uploading files (#6853) 2024-11-25 10:39:15 +08:00
Richard Shiue f013bb9d6e chore: re-add chat message animations (#6850)
* chore: re-add chat message animation

* chore: bump up message vertical padding
2024-11-24 22:43:45 +08:00
Nathan.fooo 1b4a723500 chore: only fetch billing info when current user is the workspace owner (#6847) 2024-11-23 13:25:42 +08:00
Nathan.fooo b5d2af3371 chore: parse chat response (#6843)
* chore: parse chat response

* chore: remove unrelated message

* chore: update readme
2024-11-23 09:48:24 +08:00
Nathan.fooo 4205a34f04 chore: delete upload task when upload size exceed limit (#6841)
* chore: delete upload tasks

* test: update

* chore: bump client api

* chore: fix test
2024-11-22 18:18:24 +08:00
Richard Shiue e86a9d697c chore: simplify chat user message bloc and widgets (#6836) 2024-11-20 13:13:38 +03:00
Richard Shiue f82dabcc75 chore: bump flutter chat ui version (#6835) 2024-11-20 15:47:35 +08:00
Mathias Mogensen 09717d92c5 chore: inno setup ignore version on .exe (#6745) 2024-11-20 10:03:57 +08:00
Lucas a0d8711d5c fix: close other tabs before switching workspace (#6830)
* fix: close other tabs before swtching workspace

* test: close other tabs before switching workspace

* chore: update release note

* test: close other tabs before switching workspace
2024-11-19 15:41:55 +08:00
Lucas c24b68481d feat: support creating subpage block in row detail page (#6824)
* feat: support creating subpage block in row detail page

* feat: hide the row page from sidebar

* test: support creating a sub-page block in row detail page

* fix: update drag block logic

* feat: support toggle heading in outline

* test: add toggle headings show in outline block test

* fix: unable to get focus when opening subpage from card
2024-11-19 09:58:04 +08:00
Richard Shiue df7fe9750d feat: wrap code block lines in AI chat (#6828) 2024-11-18 21:52:51 +08:00
Mathias Mogensen 9c22bb4fed fix: tab change speed + remove favorite option (#6819) 2024-11-18 17:11:39 +08:00
nathan 3803cf2506 chore: fix test 2024-11-18 16:47:45 +08:00
Nathan.fooo 51a11fbebd chore: fix upload file limit (#6825)
* chore: fix file upload limit

* chore: update test
2024-11-18 16:21:47 +08:00
Kilu.He cc7476c75b fix: add new row cover (#6826)
* fix: add new row cover

* fix: add default callout icon
2024-11-18 16:09:31 +08:00
Lucas 8a7cedd5b4 fix: cannot paste text into the title of the doc (#6821)
* fix: cannot paste text into the title of the doc

* test: add paste in title test
2024-11-18 14:44:02 +08:00
Lucas 7e528cf260 feat: support editing path name on mobile (#6798)
* feat: support editing path name on mobile

* chore: format code

* chore: update publish toast

* feat: optimize the toast for mobile more actions menu

* feat: optimize update path name logic

* test: add update path name test

* fix: integration test
2024-11-18 10:33:51 +08:00
Arya Pratap Singh 8a39ff0580 fix: turn into menu naming inconsistency issues (#6817) 2024-11-18 10:33:23 +08:00
Richard Shiue 710fbbdc08 fix: ai chat error message background color (#6810) 2024-11-17 16:04:43 +08:00
Richard Shiue 6c52896f99 chore(mobile): remove ai chat sample question autoscrolling behavior (#6793) 2024-11-17 15:36:16 +08:00
Kilu.He f526df98c7 fix: add template button to publish page (#6809) 2024-11-17 14:58:28 +08:00
Richard Shiue 521d74e082 chore: more ai ui fixes (#6799)
* chore: more ai ui fixes

* fix: messages arriving out of order

* chore: don't drop focus when clicking on sample questions
2024-11-15 17:31:51 +08:00
Lucas 225683562b fix: 0.7.4 launch review issues on mobile (#6795)
* feat: expand the hit test area for more button in space menu

* fix: contrast issue for the delte dialog text

* fix: stay in space menu after deleting a space

* fix: hide padding when space icon list is scrolled down

* feat: expand the hit test area for toggle button

* feat: remove open workspace success toast

* chore: update translations

* test: stay in space menu after deleting a space

* chore: enable android test

* fix: integration tests
2024-11-15 16:19:02 +08:00
Richard Shiue 9138787f86 chore: deprecate third party markdown (#6794) 2024-11-15 15:03:12 +08:00
Lucas 6ffb9e4d0f fix: launch 0.7.4 review issues on desktop (#6792)
* chore: enable document test runner

* fix: double menu showing in workspace menu

* feat: only display copy in ai message

* test: add test

* test: fix integration test

* fix: rust ci
2024-11-15 11:12:27 +08:00
Richard Shiue bced9327b1 feat: AI chat UI improvements (#6760)
* chore: fix typo

* chore: redundant check for cloud and use guard clause

* feat: improve ai prompt input ui

* feat: improve ai chat welcome page

* feat: improve loading status indicator

* feat: improve chat bubble avatars

* chore: clean up chat theme

* feat: improve sizing, padding and text styles

* feat: improve sugested follow up question style

* feat: improve error message style

* feat: improve mobile chat ui

* feat: add superman

* fix: cannot send new questions after 2000 character limit reached

* feat: ai response action bottom sheet

* chore: code cleanup

* feat: improve look of side panel

* chore: BIG ANNOUNCEMENT CITY

* fix: test session issues

* fix: launch review issues
2024-11-15 00:26:37 +08:00
Richard Shiue 8120656198 fix(mobile): selecting date from cupertino date widget resets time (#6791) 2024-11-15 00:26:17 +08:00
Richard ShiueandMathias Mogensen 941b7cf04c fix(flutter_desktop): document search (#6669)
* fix: double dispose on find menu

* fix: empty query not resetting search service

* fix: input focus getting lost after clicking button or pressing enter

* chore: remove unused focus node and text controller

* chore: bump appflowy editor

* chore: code cleanup

* chore: fix focus getting lost on submission

* fix: next match focuses on title after jumping

* chore: bump appflowy editor

* revert: unnecessary changes to FlowyFormTextInput

* fix: title requesting focus unexpectedly

* Update frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart

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

* chore: merge conflicts

* chore: code cleanup

* test: add integration test

* fix: show replace menu icon color in dark mode

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2024-11-14 22:02:34 +08:00
Mathias Mogensen 1952ef0853 fix: tab menu and tabbar improvements (#6785)
* fix: tab menu and tabbar improvements

* chore: update appflowy_editor

* test: tab menu test

* test: fix test after refactor
2024-11-14 06:55:09 +01:00
Lucas d9f2d14e99 chore: bump version 0.7.4 (#6787) 2024-11-14 13:36:13 +08:00
Lucas 555d08e8ce feat: add toggle heading in plus menu on mobile (#6784)
* chore: add logs in space bloc

* feat: add toggle headings in plus menu

* test: add toggle heading block test

* test: toogle heading 1 block test

* test: add toggle heading selection test

* fix: toggle headings test

* chore: update new toggle heading icons
2024-11-14 13:34:24 +08:00
Mathias Mogensen 76009613fe fix: mobile file upload menu (#6786) 2024-11-14 06:10:03 +01:00
复予 651938a322 chore: update zh-CN translations (#6717)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-11-14 09:31:25 +08:00
Lucas b86011aa94 feat: optimize the font menu speed (#6780) 2024-11-14 09:19:35 +08:00
nathan cf240a392b chore: replace with appflowy cloud main branch id 2024-11-14 00:56:51 +08:00
Nathan.fooo 28530722bc chore: bump client api (#6781)
* chore: bump client api

* chore: fix member count serde error
2024-11-14 00:51:07 +08:00
ZackandLucas.Xu 3b304747f2 feat: last publish name (#6766)
* chore: wip update client api

* feat: add unpublished information

* feat: keep the last publish name

* test: add re-publish test

* test: add empty name test

* chore: update cloud version to 0.7.6

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-11-13 20:36:35 +08:00
Lucas cd3be696dc feat: optimize the workspace menu loading speed (#6758)
* feat: optimize the workspace menu loading speed

* chore: add workspace menu animation

* feat: optimize workspace menu loading speed on mobile

* test: workspace member count test

* fix: rust ci
2024-11-13 20:36:15 +08:00
Lucas 57933736ea feat: support embed webp (#6775)
* feat: support uploading webp

* feat: support jpeg and bmp image format
2024-11-13 17:16:48 +08:00
Lucas a46550c250 feat: support managing a space on mobile (#6764)
* feat: support duplicating and deleting a space on mobile

* feat: support editing a space on mobile

* feat: support renaming space

* feat: add more space icons

* fix: unable to rename space

* fix: code review

* chore: remove ingore import flag for testing

* test: add create space test

* chore: enable cloud test

* test: fix space test

* test: add rename/duplicate/delete tests
2024-11-13 11:10:15 +08:00
Lucas eed3f489c5 chore: update template icon (#6767) 2024-11-12 16:58:41 +08:00
Lucas d268f8c715 feat: integrate codemagic for testing ios/android (#6719)
* fix: pages overflow when selecting homepage

* feat: integrate codemagic

* Revert "fix: pages overflow when selecting homepage"

This reverts commit 156882a9a7.

* chore: try to fix build

* chore: remove flutter analyuze

* chore: launch ios simulator

* fix: flutter version

* fix: integration tests on mobile

* fix: mobile tests

* test: fix page style test

* chore: enable ios ci

* chore: update codemagic token

* chore: update app_id
2024-11-12 09:06:30 +08:00
Richard Shiue 3cd26cca35 fix(flutter_desktop): hidden kanban group name overflow (#6763) 2024-11-11 17:21:24 +08:00
Zack 6ad303583b feat: include member count in get_workspace API (#6747)
* feat: add member count for workspace

* fix: sqlite migration
2024-11-08 16:01:39 +08:00
Lucas e527a1843e chore: bump version 0.7.3 (#6743)
* chore: update changelog

* feat: add 'custom namespace' in plan description
2024-11-08 10:13:17 +08:00
Richard Shiue 3f8eb70ff3 chore: bump collab (#6746) 2024-11-08 02:33:28 +08:00
Richard ShiueandMathias Mogensen bd7976d005 fix: v0.7.3 additional launch review session issues (#6730)
* fix: focusedDay on is range toggle

* fix: attempt to fix date reminder

* chore: alignment issue on windows

* chore: use onFailure

* chore: default for set reminder

* chore: code style

* fix: add reminder bug

* fix: checklist item alignment

* fix: date cell reminders

* chore: regard cell datetime as correct

* fix: reminder creation date

* chore: bump collab that fixes reminders

* test: add date cell and reminder bloc test

* chore: bump collab

* chore: revert visual density change

* fix: submitting date time text field makes it flash

* fix: improve behavior of phantom checklist item

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2024-11-07 18:33:12 +08:00
Lucas 4e1532af3e fix: application window is not visible on Windows (#6740)
* fix: try to fix the windows not visible issue

* chore: update win32_window

* chore: adjust the new api
2024-11-07 15:55:38 +08:00
Mathias Mogensen 97999aee44 fix: block option interceptor (#6739)
* fix: use unique key in interceptor

* test: add block option interaction tests
2024-11-07 09:47:47 +08:00
Nathan.fooo 6785104c3a chore: send notification for file upload state (#6738)
* chore: send notification for file upload state

* chore: fix test
2024-11-07 00:00:54 +08:00
Mathias Mogensen 193c8242e2 fix: mobile media cleanup (#6733) 2024-11-06 08:03:20 +01:00
Kilu.He 59eda5e038 fix: modified invitation page text (#6734) 2024-11-06 13:36:27 +08:00
Nathan.fooo 3c8de8a52c chore: update upload file logs (#6735) 2024-11-06 11:18:40 +08:00
Zack d38a5f38e3 chore: update dep (#6727) 2024-11-06 09:12:16 +08:00
Lucas fe4f85a597 fix: align the namespace and toggle shortcuts (#6726)
* chore: update translations, url to URL

* fix: children disapper when using toggle shortcuts

* fix: add tooltip for namespace button

* test: add toggle shortcut test
2024-11-06 09:08:59 +08:00
Mathias Mogensen 0a1af4e61f fix: 0.7.3 improvements (#6731)
* fix: recheck workspace usage on workspace changed

* fix: rename insert page item
2024-11-06 00:50:27 +01:00
Kilu.He f6e002edbd feat: support toggle heading (#6712)
* feat: support toggle heading

* fix: support others markdown
2024-11-05 14:06:38 +08:00
Lucas 82effbf8e4 fix: toggle heading issues (#6714)
* fix: pages overflow when selecting homepage

* fix: toggle heading issues

* Revert "fix: pages overflow when selecting homepage"

This reverts commit 156882a9a7.

* chore: optimize code logic

* fix: assertion in toggle list

* fix: make the turn into menu and color menu exclusive

* test: add toggle heading test
2024-11-05 10:52:22 +08:00
Lucas 54096b391f fix: settings site issues (#6701)
* fix: pages overflow when selecting homepage

* fix: settings site issues

* chore: try to fix windows ci

* test: add tests

* fix: shareblock state update

* fix: pages overflow when selecting homepage

* fix: view name doesn't update after publishing

* chore: add translations

* feat: close popup menu after unpublishing page

* feat: align the published page name with header
2024-11-05 09:23:38 +08:00
Mathias Mogensen 39eee12f53 fix: launch review 0.7.3 (#6718) 2024-11-05 00:39:00 +01:00
Mathias Mogensen bae19b9c3a feat: files media improvements (#6632)
* feat: files & media uiux improvements

* feat: desktop grid media cell changes

* test: amend tests

* feat: media mobile improvements

* feat: more refinement

* fix: dart linter

* chore: update collab

* fix: icon and test

* test: refactor upload hint

* test: forgot to use helper method
2024-11-05 00:13:19 +01:00
Richard Shiue f00e1ebf20 chore: remove focus when starting to create a card from the bottom (#6709) 2024-11-04 14:30:32 +08:00
Kilu.He 939e28d2e1 feat: support tab and shift+tab on range (#6703)
* feat: support tab and shift+tab on range

* feat: add login to no found page
2024-11-04 14:14:16 +08:00
Nathan.fooo 3fbdcab7b0 chore: try to fix when load data from cloud cause windows delay rendering (#6708) 2024-11-04 13:53:02 +08:00
Nathan.fooo a1d0dba0a5 chore: remove local ai watch on windows and linux (#6704)
* chore: remove local ai watch on windows and linux

* chore: clippy
2024-11-04 11:26:05 +08:00
Mathias Mogensen 5e41849f69 fix: remove listeners (#6686) 2024-11-04 10:26:48 +08:00
Richard Shiue cf56e20be9 chore: launch review 0.7.3 (#6698)
* refactor: date picker

* chore: provide guidance to users while using date picker

* fix: row card icon alignment

* fix: untitled database views

* chore: hide hint text while choosing date range

* test: fix widget test

* chore: use current time when toggling include time

* chore: move autofill date logic to date picker

* test: add tests

* chore: also apply to mention date block

* test: fix integration tests

* chore: fix a date picker edge case

* fix: unmatching border radii
2024-11-04 10:11:56 +08:00
Nathan.fooo 1b9b2a5f8d chore: fix database view event (#6697) 2024-11-01 21:48:25 +08:00
Nathan.fooo 690b46a59a chore: fix remove view desc test (#6696)
* chore: fix remove view desc test

* chore: fix remove view desc test

* chore: clippy
2024-11-01 15:57:58 +08:00
Richard Shiue 4d33c87b33 test: fix date picker tests (#6695) 2024-11-01 08:40:43 +01:00
Mathias Mogensen 7cf0c4b16b fix: do not rely on getIt in turnIntoBlock (#6693) 2024-11-01 08:04:02 +01:00
Lucas 910b643db0 chore: bump version 0.7.3 (#6692)
* chore: bump version 0.7.3

* fix: windows ci
2024-11-01 14:23:09 +08:00
Nathan.fooo 0e5ff844b6 chore: remove view desc (#6691)
* chore: remove desc in view

* chore: remove desc in view
2024-11-01 14:18:35 +08:00
Ahad PatelandLucas a9a784f0f0 fix: owner can delete spaces created by other users (#6672)
* fix: owner can delete spaces created by other users

* fix: Update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart

Co-authored-by: Lucas <lucas.xu@appflowy.io>

* fix: add import statement and remove conficting code

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-11-01 12:56:52 +08:00
Mathias Mogensen af390f584e feat: turn into page (sub page block) (#6685)
* feat: turn into page (sub page block)

* fix: bugs and node copyWith

* test: add base test
2024-11-01 05:14:51 +01:00
Lucas eff37d74dc fix: date reminder test (#6681) 2024-11-01 09:33:12 +08:00
Mathias Mogensen 0ea2da424f fix: reminder improvements (#6680) 2024-11-01 00:36:14 +01:00
Lucas 07e34609e7 feat: custom namespace/domain (#6614) 2024-10-31 14:38:32 +08:00
Lucas 74c757d0c8 feat: enable link to database in slash menu (#6673)
* feat: enable link to database in slash menu

* test: add link to page test

* test: remove improper test
2024-10-31 14:12:19 +08:00
Nathan.fooo e6357a9d6c chore: optimize import appflowy data (#6671)
* chore: only import user workspace

* chore: apply migration if need

* chore remove unused code

* chore: store first time installed version

* chore: clippy
2024-10-31 13:02:54 +08:00
Mathias Mogensen 5a047b9d2d fix: add referenced page hint (#6679) 2024-10-31 04:25:26 +01:00
Lucas 873ab6cdc7 feat: scroll to block after selecting notification item (#6667)
* fix: scrollbar's ScrollController has no ScrollPosition attached

* feat: support scrolling to block after selecting notification item

* chore: remove debug print

* fix: unable to cancel block selection
2024-10-31 10:29:34 +08:00
Mathias Mogensen af6736d352 fix: enable sub page block (#6595)
* fix: enable sub page block

* fix: open newly inserted page

* fix: created view should have empty name

* test: use secondary to rename page

* fix: make popover secondary interaction better

* test: amend test

* fix: icon color of sub page block

* test: fix tests

* test: fix hover issue

* feat: clean API for show at cursor on popover
2024-10-30 22:29:01 +01:00
Richard Shiue d5c1955ea3 chore: capitalization for field editor insert left/right (#6676) 2024-10-31 00:12:19 +08:00
Richard Shiue 8bb8131dd4 feat(flutter_desktop): press enter to confirm a dialog (#6675)
* feat: enter to confirm in popup dialog

* test: add test
2024-10-30 23:20:12 +08:00
Richard Shiue 7f4abb5866 chore(flutter_desktop): drop down menu conforms to design (#6674) 2024-10-30 23:19:05 +08:00
Richard Shiue fbe87cc536 fix(flutter_desktop): add forgotten call to update text cell (#6668) 2024-10-30 19:31:10 +08:00
Kilu.He 74165ace0f fix: optimize bundle (#6666)
* feat: open editing

* fix: use cdn to replace build react and react-dom

* fix: replace mention page icon

* fix: replace optimize web bundle
2024-10-30 14:13:52 +08:00
proxycon b576287ac6 chore: update translations with Fink 🐦 (#6656) 2024-10-30 09:29:05 +08:00
Richard Shiue 68d7211735 fix(flutter_desktop): glitchy kanban card editing (#6661)
* test: add test

* fix(flutter_desktop): editing kanban card is glitchy
2024-10-29 22:46:22 +08:00
Richard Shiue dd0dcace87 chore(flutter_desktop): set VisualDensity.standard for entire desktop app (#6648)
* chore: set standard visual density for entire app

* fix: pixel overflow

* fix: pixel overflow

* fix: pixel overflow

* fix: pixel overflow

* fix: text button padding

* chore: slash menu fixes

* chore: slash menu fixes

* ditto
2024-10-29 21:26:04 +08:00
Richard Shiue 8f58c39448 chore(flutter_desktop): improve title and heading appearance (#6658) 2024-10-29 21:25:13 +08:00
Nathan.fooo cee0e2ed42 test: fix ci (#6657) 2024-10-29 20:55:37 +08:00
Lucas 557ce7e006 chore: update appflowy_editor version (#6655)
* fix: foramt link starting at 0 index will lose href attr

* fix: pillcrow icon resize

* fix: integration test

* fix: emoji may not align on Linux
2024-10-29 19:39:56 +08:00
Nathan.fooo d50521d9b6 chore: ignore invalid database when import appflowy data (#6654) 2024-10-29 16:20:05 +08:00
Lucas 28aa2329fb feat: run integration tests on multiple runners (#6637)
* feat: split the integration tests into multiple runners

* feat: use 6 runners

* fix: name

* fix: integration tests

* fix: macos arm64 build

* Revert "fix: macos arm64 build"

This reverts commit 80b961361f.

* chore: use 7 runners

* chore: use 8 runners
2024-10-29 14:55:20 +08:00
Nathan.fooo 9707148b86 chore: bump appflowy collab (#6649)
* chore: fix test

* chore: fix duplicated view name

* chore: fix windows build

* chore: clippy
2024-10-29 11:21:49 +08:00
Lucas 4e739c857b fix: macos arm64 build (#6650) 2024-10-29 10:20:48 +08:00
Nathan.fooo 363fecc7d6 chore: bump client api that includes upload large zip file (#6634)
* chore: bump client api

* chore: update cloud version on CI

* chore: bump client api

* chore: update ci
2024-10-28 21:51:07 +08:00
Kilu.He 6bbaad59a4 fix: redirect to default page (#6638)
* feat: open editing

* fix: add logs

* fix: add logs
2024-10-28 17:55:11 +08:00
Kilu.He 497fcf3bbb fix: support redirect to default page (#6593) 2024-10-28 14:15:59 +08:00
Kilu.He b9ec9f863d fix: import interfaces and error message (#6630) 2024-10-28 11:34:56 +08:00
rensawamo eafd0b3353 fix: crash on delete button press caused by GoRouter (#6629)
* fix: crash on delete button press caused by GoRouter

* fix: gorouter pop
2024-10-28 09:09:11 +08:00
Nathan.fooo a42b6e02ab chore: revamp error for FolderCloudService service (#6623)
* chore: revamp error for FolderCloudService service

* chore: bump cloud

* ci: update configuration

* chore: clippy

* chore: bump client api
2024-10-26 11:20:16 +08:00
Mathias Mogensen 79116e7c7f fix: use drop path rather than node (#6611) 2024-10-25 14:05:19 +02:00
Richard Shiue 128ff594be fix: date picker code improvements and regressions (#6619)
* chore: improve handling result's error variant

* chore: dont call setState in didUpdateWidget

* fix: don't make assumptions on provided data
2024-10-24 20:46:45 +08:00
Lucas c821454df8 Revert "feat: custom domain (#6518)" (#6613)
This reverts commit 73e8b476fb.
2024-10-24 11:10:59 +08:00
ZackandLucas.Xu 73e8b476fb feat: custom domain (#6518)
* feat: api for namespace setting

* feat: add publish timestamp

* feat: update api

* chore: update proto

* feat: add link impl for appflowy cloud

* feat: impl set and get default view for workspace

* chore: merge with main and update to latest cloud client api

* fix: ci build

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-10-23 19:41:46 +08:00
Lucas faa95ffb9c feat: support toggle heading (#6598)
* feat: support toggle heading

* feat: add numbered list shortcut and heading shortcut

* feat: update format greater to toggle list shortcut

* feat: add removeToggleHeadingStyle shortcut

* feat: add toggle list and toggle headings in turn into menu

* feat: turn into toggle heading block

* feat: highlight selected toggle heading

* fix: collapsed button animation

* test: turn heading block to toggle heading block

* test: turn toggle heading block to heading block

* test: convert '# + >' or '> + #' to toggle heading block

* test: turn heading 2 block with following heading 1 block to toggle heading 2 block

* test: integration tests

* fix: unable to move children after heading 1 to toggle heading block

* test: unable to move children after heading 1 to toggle heading block

* fix: flutter analyze

* fix: toggle heading block test: insert toggle heading 1 - 3 block by shortcuts

* fix: inline database in document tests

* fix: integration test
2024-10-23 19:41:12 +08:00
Richard Shiue 6a5c4c95b7 feat(flutter): date picker improvements (#6582)
* feat(flutter): improve date picker

* test: add widget tests

* test: add bloc test

* test: adjust rust-lib tests

* fix: fix failing tests

* chore: fix rustfmt

* test: remove redundant await

* fix: fix failing tests

* feat: use cupertino date picker on mobile

* fix: flutter analyzer
2024-10-23 08:51:37 +08:00
Richard Shiue 7141e9d8cf fix(flutter-moile): edit primary field from database controls (#6591) 2024-10-22 20:22:15 +08:00
Richard Shiue d7bbb4261c chore: remove legacy CreateRowParams (#6592) 2024-10-22 20:22:03 +08:00
Lucas 251b16cb62 chore: update changelog (#6596) 2024-10-22 10:22:25 +08:00
Mathias Mogensen da80c599bf fix: show default view name (#6571) 2024-10-22 09:31:14 +08:00
Lucas ca6a900874 fix: display paragraph mark only if the block content is empty (#6594) 2024-10-21 20:25:55 +08:00
Lucas 5e09e96f7b fix: callout emoji size should follow the document font size (#6590) 2024-10-21 18:00:32 +08:00
Mathias MogensenandLucas.Xu 068500df84 feat: inline sub page mention (#6567)
* feat: inline sub page mention

* fix: disable editing documents in trash

* fix: duplicate block behavior

* refactor: clean up code

* feat: use formatText function instead of modify delta manually

* fix: paste behavior format mention

* fix: default icon for mentioned pages

* fix: view new parent turn into page reference

* test: add base test

* chore: add feature flag

* chore: default flag to on

* fix: minor fixes to behavior

* fix: review and code cleanup

* fix: dart linter

* fix: content is required

* test: use doc title to rename page

* test: add test coverage

* test: fix wrong expect

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-10-21 16:34:30 +08:00
Murat Sarıbaş 15949a7e21 chore: update translations with Fink 🐦 (#6565) 2024-10-21 09:45:52 +08:00
Mathias Mogensen e635fe3378 fix: trash breadcrumb line height (#6588) 2024-10-21 09:42:48 +08:00
Mathias Mogensen 8276c5db57 fix: drag and drop row document full page (#6589) 2024-10-21 09:42:15 +08:00
Mathias Mogensen 1d9876a6eb fix: notifier disposed but in use (#6586) 2024-10-21 00:21:25 +02:00
Lucas 6340174927 fix: share link and publish issues on mobile (#6579)
* fix: cache publish info to avoid repeated requests

* chore: 'Copy Link' to 'Copy link'

* chore: enable ios ci

* fix: ios ci

* chore: replace mention block icon

* fix: ios ci tests

* fix: ios ci tests

* fix: ai wirte replace assertion

* fix: windows ci
2024-10-20 10:13:06 +08:00
Lucas 7b031b228e fix: turn into issues (#6576)
* fix: cover title issues

* fix: the selection should be cleared if selecting child node

* fix: exclude the blocks that are not supported in the 'turn into' types

* fix: add logs

* fix: floating toolbar ai status

* test: selecting the parent should deselect all the child nodes as well

* chore: 'Copy Link' to 'Copy link'

* fix: select all and turn into block doesn't work on Windows

* test: calculate turn into selection test

* fix: option button tests
2024-10-18 17:13:38 +08:00
Richard Shiue ec408940e4 fix(flutter_desktop): bring back the board scrollbar (#6580) 2024-10-18 15:52:32 +08:00
Richard Shiue bd46fc11f4 fix(flutter-desktop): launch review issues for 0.7.2 (#6577)
* test: add test case for calendar filter

* fix: select option filter logic

* fix: calendar filters not applying if switched repeatedly

* fix: checklist cell editor improvements

* fix: nested scrolling in checklist

* fix: make filter logic match notion

* test: fix checklist cell test

* test: single select filter tests

* test: fix flutter test

* test: fix rust tests

* chore: fix clippy
2024-10-18 13:48:50 +08:00
nathan 42e55fe248 chore: update import string 2024-10-17 16:03:22 +08:00
nathan 3a9011b3cb chore: remove timezone for self-hosted runner 2024-10-17 15:42:01 +08:00
Lucas e3bf0442e8 fix: space icon color assertion (#6570)
* fix: space icon color assertion

* chore: bump version 0.7.2
2024-10-17 13:43:14 +08:00
Lucas 0413100e2b feat: observe mention block change and support block navigation (#6568)
* feat: observe mentioned block changes and navigate to block

* test: add delete mentioned block test

* chore: update editor version

* feat: navigate block in same page

* fix: sometimes turn into menu doesn't work

* test: add test

* fix: integration test
2024-10-17 13:29:34 +08:00
nathan 7cad04bbf4 chore: bump collab which include fixing windows build 2024-10-17 13:27:05 +08:00
Mathias Mogensen c9f0bbf26a feat: right click to open view more actions (#6529)
* feat: right click to open view more actions

* test: fix tests

* test: fix failing test

* fix: hide in move to page menu

* test: add simple test

* test: add negative case in cloud test

* chore: check code related to sidebar spaces

* fix: view item

* fix: clean up
2024-10-17 06:58:01 +02:00
Nathan.fooo d91639172a chore: show import error (#6569) 2024-10-17 10:32:51 +08:00
Nathan.fooo 5f0cc1becd chore: bump client api and collab (#6563)
* chore: bump client api and collab

* chore: fix windows build
2024-10-17 09:45:45 +08:00
Lucas a8bcab7770 feat: turn into multiple lines (#6558)
* feat: select multiple lines with block selection style

* feat: multiple nodes conversion

* fix: exclude children for the block can't contain children

* chore: update editor version

* fix: unit test

* test: convert nested list to heading/quote/callout

* test: transform nodes at the same level into another block type

* test: add undo redo for turn into

* test: add multi lines integration test

* chore: remove debug logs

* fix: integration test
2024-10-16 22:19:18 +08:00
Kilu.He b1682e4f54 fix: modified import function (#6562) 2024-10-16 21:40:32 +08:00
Richard Shiue 2447b34107 fix: text button's text style (#6559) 2024-10-16 17:06:14 +08:00
Richard Shiue 8126d19682 fix(flutter): wrong time zone when filtering by date field (#6543)
* fix: wrong time zone when filtering by date field

* test: set time zone
2024-10-16 16:44:06 +08:00
Richard Shiue 6cc8c81e3d chore: adjust Toggle widget's onChanged callback (#6556) 2024-10-16 14:39:46 +08:00
Nathan.fooo e86d812ee6 chore: add tooltip when import notion file (#6557) 2024-10-16 13:55:21 +08:00
Nathan.fooo c6f042830f fix: import document that contains refs (#6551)
* fix: import document that contains refs

* chore: clippy
2024-10-15 23:55:27 +08:00
Nathan.fooo 2378c0c441 chore: add calculation rust test (#6547)
* chore: add test

* chore: clippy
2024-10-15 17:20:53 +08:00
Lucas 7bc53d7bc6 feat: support 'share link' and 'publish' on mobile (#6548)
* feat: support publish on mobile

* chore: replace icons

* feat: support publish and copy link on mobile

* feat: support copy link

* feat: support copy share link

* chore: update icons

* fix: flutter analyze

* fix: ignore opening same page on mobile

* test: add copy link test on mobile
2024-10-15 15:12:59 +08:00
Nathan.fooo 7918d6c0d1 chore: update logs (#6550) 2024-10-15 15:05:22 +08:00
Kilu.He 073e8b897b feat: support import notion (#6549)
* feat: support import notion

* feat: support import page
2024-10-15 11:49:38 +08:00
Nathan.fooo 46a3006f4d refactor: database test (#6544)
* chore: remove script

* chore: remove script

* chore: refactor test

* chore: clippy

* chore: fix test

* chore: fix test

* chore: fmt
2024-10-15 09:32:06 +08:00
Deep Isane 5d578ab9e6 chore: update README.md fixed a typo (#6546)
Corrected "Acknowledgements" to "Acknowledgments" in [README.md].

This pull request addresses a minor typo found in the repository. The typo has been corrected to improve clarity and maintain the quality of the documentation.

This change is purely cosmetic and does not affect functionality.
2024-10-14 22:11:34 +02:00
Lucas f3fe729900 fix: potentially missing field in todo list (#6540)
* fix: potentially missing field in todo list

* chore: upgrade appflowy_editor_plugins

* fix: potentially missing field in todo list
2024-10-14 20:54:13 +08:00
Ahad PatelandLucas.Xu a04c157770 fix: date issue when enbling time feild (#6447)
* fix: date issue when enbling time field

* chore: add integration test for date or reminder field

* chore: add test file to document_test_runner.dart

* fix: flutter analyze

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-10-14 20:54:03 +08:00
Nathan.fooo 320ffcb434 Import notion zip (#6528)
* chore: upgrade client api

* chore: import notion zip

* chore: bump client api

* chore: bump collab

* chore: upgrade cloud version

* chore: fix test

* chore: fix test
2024-10-14 20:38:04 +08:00
Lucas 37a2fb17b5 feat: copy link to block (#6523)
* feat: copy link to block

* chore: add comment

* feat: using mention bloc to manage the mention bloc status

* feat: use mention page bloc to manage mentioned page status

* feat: observe mention block content changes

* feat: sync the block content

* fix: integration test

* fix: mentioned block display name contains unnessary -

* fix: handle block was deleted case

* chore: move the get doc block and get mentioned page status to service

* feat: support copy link to page

* test: add copy link to block tests

* test: add copy link to block(same pge) tests

* test: open the mentioned page

* fix: integration test

* Revert "fix: integration test"

This reverts commit f4466b22d8.

* fix: integration test
2024-10-14 14:04:42 +08:00
Kilu.He b5936cec54 fix: supported incremental updates (#6531) 2024-10-11 11:29:29 +08:00
Richard Shiue ea61c81cce feat(flutter_desktop): open created filtered rows (#6522)
* fix: revert previous implementation

* feat: open created but hidden rows as page

* fix: cargo clippy

* test: add integration tests

* fix: typo

* fix: flutter analyzer

* chore: clean up code

* chore: code cleanup

* chore: code cleanup
2024-10-11 11:04:03 +08:00
Kilu.He 64d71d0d20 fix: add sync manager (#6517)
* fix: add sync manager

* fix: add toolbar actions

* fix: add import entry

* fix: modified approve page
2024-10-10 19:48:30 +08:00
Mathias Mogensen af08b4c004 fix: hide breadcrumbs for deleted pages and show trash (#6512)
* fix: hide breadcrumbs for deleted pages and show trash

* fix: copy for banner and trash deletion

* test: add base test

* test: expand on test

* test: fix test

* test: fix failing test
2024-10-10 11:57:42 +02:00
ea670b6ae6 fix: grid new row getting cut (#6475)
* fix: Grid new row getting cut

* chore: add test case for testing text in veiwport

* refac: clean up code

---------

Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-10-10 11:02:05 +02:00
Lucas f19e354418 feat: support 'turn into' in doc (#6516)
* feat: customize animation for popover

* chore: code refactor

* feat: using popover direction calculate the popover animation translate direction

* feat: integrate the animated popover in appflowy_popover and popover_action

* fix: close popover assertion

* chore: format code

* chore: code refactor

* feat: optimize the popover listener

* feat: clear popover when hot-reloading

* chore: refactor code

* feat: integrate animated popover in block action button

* fix: integration test

* feat: add turn into entry

* fix: popover asBarrier issue

* feat: move biz logic from widget to cubit

* feat: add turn into menu

* chore: remove unused code

* feat: support h1-h3

* feat: add block conversions

* fix: integration test

* feat: implement block conversions

* fix: outline test

* test: add turn into tests

* test: add turn into nested list test

* test: add turn into nested list test

* fix: flutter analyze

* chore: replace turninto icon

* feat: integrate animated popover in color option button

* chore: split the block option action into separate files

* test: add integration test

* fix: outline block test

* fix: integration test

* fix: shortcut test
2024-10-10 14:40:38 +08:00
Richard Shiue b54e3dd243 feat: checklist cell improvements (#6515)
* feat: reorder checklist tasks

* feat: quickly create checklist tasks

* feat: reorder checklist on mobile

* test: add integration test

* test: add bloc test

* fix: flutter analyzer

* fix: cargo clippy
2024-10-10 11:38:17 +08:00
Richard Shiue 9798e39e49 feat: calendar filters (#6519)
* feat: calendar filters

* test: add integration tests
2024-10-10 11:00:23 +08:00
Richard Shiue 66fd0c19da test: fix flaky sort editor bloc test (#6520) 2024-10-10 11:00:09 +08:00
Mathias Mogensen 8d6c67206c feat: enable drag and drop in row document (#6513)
* feat: enable drag and drop in row document

* refactor: cleanup drop handling to one widget

* fix: clean up code
2024-10-09 15:23:42 +02:00
Lucas 8cf683eb50 feat: customize animation for popover (#6507)
* feat: customize animation for popover

* chore: code refactor

* feat: using popover direction calculate the popover animation translate direction

* feat: integrate the animated popover in appflowy_popover and popover_action

* fix: close popover assertion

* chore: format code

* chore: code refactor

* feat: optimize the popover listener

* feat: clear popover when hot-reloading

* chore: refactor code

* fix: integration test

* fix: icon test
2024-10-09 15:10:05 +08:00
Kilu.He 580a23f3f5 feat: support pasted on web (#6510)
* fix: add some test cases

* fix: with pasted plugin
2024-10-09 11:50:33 +08:00
Richard Shiue 29dc46a993 fix: dont edit every view while handling database view row change (#6506)
* fix: don't process on every view while only handling one view

* test: add test
2024-10-09 11:01:09 +08:00
Richard Shiue 93885bad7b fix: get all calendar events not fetching empty ones (#6508) 2024-10-09 11:00:33 +08:00
Richard Shiue 6e0619c21e chore: bump validator crate version (#6494) 2024-10-08 21:38:58 +08:00
Lucas 8c956afabd fix: show AI limit error toast if exceeding the AI response (#6505)
* fix: show AI limit error toast if exceeding the AI response

* test: add ai limit test
2024-10-08 18:28:07 +08:00
Richard Shiue f9fbf62283 test: fix flaky sort editor bloc test (#6504)
* test: fix flaky sort test

* test: experiment

* test: remove loops
2024-10-08 16:33:48 +08:00
Lucas 5f1f536181 feat: support editing name when creating a new page on mobile (#6501)
* feat: support editing name when creating a new page on mobile

* chore: add defaultName in layout extension

* test: add cover title test on mobile

* fix: cover title test on mobile

* feat: add integration runner 4

* chore: update translations

* chore: disable subpage feature
2024-10-08 14:29:07 +08:00
Lucas ba59514464 fix: ignore same workspace icon update (#6496)
* fix: ignore same workspace icon update

* feat: pressing esc to exit editing mode in cover title

* fix: integration test

* fix: unable to use redo/undo in cover title

* chore: update collab version b56d051

* feat: show add emoji & add cover button when hovering on the title

* test: add hover on title test

* fix: unable to use arrow right in cover title

* fix: unable to select title sometimes

* fix: integration test

* fix: cloud integration test

* fix: cloud integration test

* fix: integration test
2024-10-08 11:46:15 +08:00
Annie 6bb6750be7 chore: update CHANGELOG.md (#6502)
change log for v0.7.1
2024-10-08 09:22:34 +08:00
Kilu.He 19a3df6d47 feat: support shortcuts on web (#6474)
* fix: add tab and shift+tab test cases on web

* fix: support BIUS on web

* fix: add BIUS test cases

* fix: add markdown to block test cases

* fix: add markdown to block test cases
2024-10-07 18:30:39 +08:00
Mathias Mogensen 87408fd2a9 fix: sub page block review fixes (#6491)
* fix: sub page block review fixes

* test: fix tests
2024-10-07 12:08:44 +02:00
Richard Shiue 9ee39f45c9 test: rewrite bloc tests (#6492) 2024-10-07 16:41:20 +08:00
Lucas a763304386 fix: fix potential text empty when editing doc (#6495) 2024-10-07 16:40:36 +08:00
Nathan.fooo f8ce501b53 chore: fix anon user integration test (#6493)
* chore: fix anon user integration test

* chore: fix env

* chore: appflowy cloud version

* chore: appflowy cloud version
2024-10-07 13:31:47 +08:00
nathan 54d449647c fix: clippy and test 2024-10-07 11:20:59 +08:00
Lucas 552fd39d74 fix: share button doesn't response under macOS 15 (#6487)
* fix: share button doesn't response under macos 15

* chore: enable macos integration test

* Revert "chore: enable macos integration test"

This reverts commit 32c49c9f36.

* chore: update comment
2024-10-07 10:18:11 +08:00
nathan 01d73f2753 chore: clippy 2024-10-07 09:44:00 +08:00
Nathan.fooo 75da496128 fix: calculation bug (#6489)
* chore: upgrade collab

* fix: calculate value

* chore: optimize calculate

* chore: cal when open database

* chore: update calculation when filter change

* chore: use same runtime
2024-10-07 08:57:42 +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
Kilu.He 4d4e3bfb8a fix: add backspace behavior test cases (#6463) 2024-10-03 22:37:40 +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
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
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
1820 changed files with 99544 additions and 38653 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
@@ -52,7 +52,7 @@ runs:
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 network-manager libmpv-dev mpv
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 network-manager
shell: bash
- name: Enable Flutter Desktop
@@ -75,4 +75,4 @@ runs:
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test ${{ inputs.test_path }} -d Linux --coverage
shell: bash
shell: bash
+179 -108
View File
@@ -1,126 +1,197 @@
# name: Android CI
name: Android CI
# on:
# push:
# branches:
# - "main"
# paths:
# - ".github/workflows/mobile_ci.yaml"
# - "frontend/**"
# - "!frontend/appflowy_tauri/**"
on:
push:
branches:
- "main"
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
# pull_request:
# branches:
# - "main"
# paths:
# - ".github/workflows/mobile_ci.yaml"
# - "frontend/**"
# - "!frontend/appflowy_tauri/**"
pull_request:
branches:
- "main"
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
# env:
# CARGO_TERM_COLOR: always
# FLUTTER_VERSION: "3.22.0"
# RUST_TOOLCHAIN: "1.77.2"
# CARGO_MAKE_VERSION: "0.36.6"
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.80.1"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64
# concurrency:
# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
# cancel-in-progress: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# jobs:
# build:
# if: github.event.pull_request.draft != true
# strategy:
# fail-fast: true
# matrix:
# os: [macos-14]
# runs-on: ${{ matrix.os }}
jobs:
build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
# steps:
# - name: Check storage space
# run: df -h
steps:
- name: Check storage space
run:
df -h
# # the following step is required to avoid running out of space
# - name: Maximize build space
# if: matrix.os == 'ubuntu-latest'
# run: |
# sudo rm -rf /usr/share/dotnet
# sudo rm -rf /opt/ghc
# sudo rm -rf "/usr/local/share/boost"
# sudo rm -rf "$AGENT_TOOLSDIRECTORY"
# sudo docker image prune --all --force
# sudo rm -rf /opt/hostedtoolcache/codeQL
# sudo rm -rf ${GITHUB_WORKSPACE}/.git
# sudo rm -rf $ANDROID_HOME/ndk
# the following step is required to avoid running out of space
- name: Maximize build space
if: matrix.os == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
# - name: Check storage space
# run: df -h
- name: Check storage space
run: df -h
# - name: Checkout source code
# uses: actions/checkout@v4
- name: Checkout appflowy cloud code
uses: actions/checkout@v4
with:
repository: AppFlowy-IO/AppFlowy-Cloud
path: AppFlowy-Cloud
# - uses: actions/setup-java@v4
# with:
# distribution: temurin
# java-version: 11
- name: Prepare appflowy cloud env
working-directory: AppFlowy-Cloud
run: |
# log level
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: Install Rust toolchain
# id: rust_toolchain
# uses: actions-rs/toolchain@v1
# with:
# toolchain: ${{ env.RUST_TOOLCHAIN }}
# override: true
# profile: minimal
- name: Run Docker-Compose
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
else
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
echo "AppFlowy-Cloud is running with an incorrect version. Restarting with the correct version..."
# Remove all containers if any exist
if [ "$(docker ps -aq)" ]; then
docker rm -f $(docker ps -aq)
else
echo "No containers to remove."
fi
# - name: Install flutter
# id: flutter
# uses: subosito/flutter-action@v2
# with:
# channel: "stable"
# flutter-version: ${{ env.FLUTTER_VERSION }}
# Remove all volumes if any exist
if [ "$(docker volume ls -q)" ]; then
docker volume rm $(docker volume ls -q)
else
echo "No volumes to remove."
fi
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
docker ps -a
docker compose logs
else
echo "AppFlowy-Cloud is running with the correct version."
fi
fi
# - uses: gradle/gradle-build-action@v3
# with:
# gradle-version: 7.4.2
- name: Checkout source code
uses: actions/checkout@v4
# - uses: davidB/rust-cargo-make@v1
# with:
# version: "0.36.6"
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 11
# - name: Install prerequisites
# working-directory: frontend
# run: |
# rustup target install aarch64-linux-android
# rustup target install x86_64-linux-android
# cargo install --force duckscript_cli
# cargo install cargo-ndk
# 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
# sudo apt-get install keybinder-3.0 libnotify-dev
# sudo apt-get install gcc-multilib
# 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
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
# - name: Build AppFlowy
# working-directory: frontend
# run: |
# cargo make --profile development-android appflowy-android-dev-ci
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- uses: gradle/gradle-build-action@v3
with:
gradle-version: 8.10
# - name: Run integration tests
# # https://github.com/ReactiveCircus/android-emulator-runner
# uses: reactivecircus/android-emulator-runner@v2
# with:
# api-level: 32
# arch: arm64-v8a
# disk-size: 2048M
# working-directory: frontend/appflowy_flutter
# script: flutter test integration_test/runner.dart
- uses: davidB/rust-cargo-make@v1
with:
version: ${{ env.CARGO_MAKE_VERSION }}
- name: Install prerequisites
working-directory: frontend
run: |
rustup target install aarch64-linux-android
rustup target install x86_64-linux-android
rustup target add armv7-linux-androideabi
cargo install --force duckscript_cli
cargo install cargo-ndk
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
sudo apt-get install keybinder-3.0 libnotify-dev
sudo apt-get install gcc-multilib
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
- name: Build AppFlowy
working-directory: frontend
run: |
cargo make --profile development-android appflowy-core-dev-android
cargo make --profile development-android code_generation
cd rust-lib
cargo clean
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run integration tests
# https://github.com/ReactiveCircus/android-emulator-runner
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
arch: x86_64
disk-size: 2048M
working-directory: frontend/appflowy_flutter
disable-animations: true
force-avd-creation: false
target: google_apis
script: flutter test integration_test/mobile/cloud/cloud_runner.dart
+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
+41 -88
View File
@@ -25,9 +25,10 @@ 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"
CLOUD_VERSION: 0.6.54-amd64
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -39,7 +40,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -73,7 +74,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ windows-latest ]
os: [windows-latest]
include:
- os: windows-latest
flutter_profile: development-windows-x86
@@ -100,7 +101,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ macos-latest ]
os: [macos-latest]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
@@ -122,12 +123,12 @@ jobs:
flutter_profile: ${{ matrix.flutter_profile }}
unit_test:
needs: [ prepare-linux ]
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -173,7 +174,7 @@ jobs:
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
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
fi
shell: bash
@@ -216,11 +217,11 @@ jobs:
shell: bash
cloud_integration_test:
needs: [ prepare-linux ]
needs: [prepare-linux]
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -241,12 +242,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: ${{ env.CLOUD_VERSION }}
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
@@ -258,11 +262,26 @@ jobs:
else
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
echo "AppFlowy-Cloud is running with an incorrect version. Restarting with the correct version..."
# Remove all containers if any exist
if [ "$(docker ps -aq)" ]; then
docker rm -f $(docker ps -aq)
else
echo "No containers to remove."
fi
# Remove all volumes if any exist
if [ "$(docker volume ls -q)" ]; then
docker volume rm $(docker volume ls -q)
else
echo "No volumes to remove."
fi
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
docker ps -a
docker compose logs
else
echo "AppFlowy-Cloud is running with the correct version."
fi
@@ -289,7 +308,7 @@ jobs:
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
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
shell: bash
- name: Enable Flutter Desktop
@@ -317,96 +336,30 @@ 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
integration_test_1:
needs: [ prepare-linux ]
integration_test:
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
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
uses: actions/checkout@v4
- name: Install video dependency
run: |
sudo apt-get update
sudo apt-get -y install libmpv-dev mpv
shell: bash
- name: Flutter Integration Test 1
- name: Flutter Integration Test ${{ matrix.test_number }}
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_1.dart
test_path: integration_test/desktop_runner_${{ matrix.test_number }}.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
integration_test_2:
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install video dependency
run: |
sudo apt-get update
sudo apt-get -y install libmpv-dev mpv
shell: bash
- name: Flutter Integration Test 2
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_2.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
integration_test_3:
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install video dependency
run: |
sudo apt-get update
sudo apt-get -y install libmpv-dev mpv
shell: bash
- name: Flutter Integration Test 3
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_3.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
+20 -16
View File
@@ -20,7 +20,7 @@ on:
- "!frontend/appflowy_web_app/**"
env:
FLUTTER_VERSION: "3.22.0"
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.80.1"
concurrency:
@@ -48,13 +48,13 @@ jobs:
model: "iPhone 15"
shutdown_after_job: false
build-macos:
integration-tests:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: macos-13
runs-on: macos-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -102,16 +102,20 @@ jobs:
model: "iPhone 15"
shutdown_after_job: false
# - name: Run AppFlowy on simulator
# working-directory: frontend/appflowy_flutter
# run: |
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
# pid=$!
# sleep 500
# kill $pid
# continue-on-error: true
- name: Run AppFlowy on simulator
working-directory: frontend/appflowy_flutter
run: |
flutter run -d ${{ steps.simulator-action.outputs.udid }} &
pid=$!
sleep 500
kill $pid
continue-on-error: true
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
# Integration tests
- name: Run integration tests
working-directory: frontend/appflowy_flutter
# The integration tests are flaky and sometimes fail with "Connection timed out":
# Don't block the CI. If the tests fail, the CI will still pass.
# Instead, we're using Code Magic to re-run the tests to check if they pass.
continue-on-error: true
run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
+83
View File
@@ -0,0 +1,83 @@
name: Mobile-CI
on:
workflow_dispatch:
inputs:
branch:
description: "Branch to build"
required: true
default: "main"
workflow_id:
description: "Codemagic workflow ID"
required: true
default: "ios-workflow"
type: choice
options:
- ios-workflow
- android-workflow
env:
CODEMAGIC_API_TOKEN: ${{ secrets.CODEMAGIC_API_TOKEN }}
APP_ID: "6731d2f427e7c816080c3674"
jobs:
trigger-mobile-build:
runs-on: ubuntu-latest
steps:
- name: Trigger Codemagic Build
id: trigger_build
run: |
RESPONSE=$(curl -X POST \
--header "Content-Type: application/json" \
--header "x-auth-token: $CODEMAGIC_API_TOKEN" \
--data '{
"appId": "${{ env.APP_ID }}",
"workflowId": "${{ github.event.inputs.workflow_id }}",
"branch": "${{ github.event.inputs.branch }}"
}' \
https://api.codemagic.io/builds)
BUILD_ID=$(echo $RESPONSE | jq -r '.buildId')
echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT
echo "build_id=$BUILD_ID"
- name: Wait for build and check status
id: check_status
run: |
while true; do
curl -X GET \
--header "Content-Type: application/json" \
--header "x-auth-token: $CODEMAGIC_API_TOKEN" \
https://api.codemagic.io/builds/${{ steps.trigger_build.outputs.build_id }} > /tmp/response.json
RESPONSE_WITHOUT_COMMAND=$(cat /tmp/response.json | jq 'walk(if type == "object" and has("subactions") then .subactions |= map(del(.command)) else . end)')
STATUS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.build.status')
if [ "$STATUS" = "finished" ]; then
SUCCESS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.success')
BUILD_URL=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.buildUrl')
echo "status=$STATUS" >> $GITHUB_OUTPUT
echo "success=$SUCCESS" >> $GITHUB_OUTPUT
echo "build_url=$BUILD_URL" >> $GITHUB_OUTPUT
break
elif [ "$STATUS" = "failed" ]; then
echo "status=failed" >> $GITHUB_OUTPUT
break
fi
sleep 60
done
- name: Slack Notification
uses: 8398a7/action-slack@v3
if: always()
with:
status: ${{ steps.check_status.outputs.success == 'true' && 'success' || 'failure' }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
text: |
Mobile CI Build Result
Branch: ${{ github.event.inputs.branch }}
Workflow: ${{ github.event.inputs.workflow_id }}
Build URL: ${{ steps.check_status.outputs.build_url }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }}
+11 -11
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
@@ -368,7 +368,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install -y alien libnotify-dev libmpv-dev mpv
sudo apt-get install -y alien libnotify-dev
source $HOME/.cargo/env
cargo install --force cargo-make
cargo install --force duckscript_cli
+34 -21
View File
@@ -15,11 +15,10 @@ on:
- "main"
- "develop"
- "release/*"
paths:
- "frontend/rust-lib/**"
env:
CARGO_TERM_COLOR: always
CLOUD_VERSION: 0.7.6-amd64
RUST_TOOLCHAIN: "1.77.2"
jobs:
@@ -42,11 +41,14 @@ jobs:
cp deploy.env .env
sed -i '' 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i '' 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
sed -i '' 's|APPFLOWY_AI_OPENAI_API_KEY=.*|APPFLOWY_AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}|' .env
- name: Ensure AppFlowy-Cloud is Running with Correct Version
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
@@ -63,6 +65,8 @@ jobs:
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
docker ps -a
docker compose logs
else
echo "AppFlowy-Cloud is running with the correct version."
fi
@@ -91,6 +95,11 @@ jobs:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Set timezone for action
uses: szenius/set-timezone@v2.0
with:
timezoneLinux: "US/Pacific"
- name: Maximize build space
run: |
sudo rm -rf /usr/share/dotnet
@@ -127,33 +136,37 @@ 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_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
# Remove all containers if any exist
if [ "$(docker ps -aq)" ]; then
docker rm -f $(docker ps -aq)
else
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
else
echo "AppFlowy-Cloud is running with the correct version."
fi
echo "No containers to remove."
fi
# Remove all volumes if any exist
if [ "$(docker volume ls -q)" ]; then
docker volume rm $(docker volume ls -q)
else
echo "No volumes to remove."
fi
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
docker ps -a
docker compose logs
- name: Run rust-lib tests
working-directory: frontend/rust-lib
env:
+94 -1
View File
@@ -1,4 +1,97 @@
# Release Notes
## Version 0.7.5 - 25/11/2024
### Bug Fixes
- Improved chat response parsing
- Fixed toggle list icon direction for RTL mode
- Fixed cross blocks formatting not reflecting in float toolbar
- Fixed unable to click inside the toggle list to create a new paragraph
- Fixed open file error 50 on macOS
- Fixed upload file exceed limit error
## Version 0.7.4 - 19/11/2024
### New Features
- Support uploading WebP and BMP images
- Support managing workspaces on mobile
- Support adding toggle headings on mobile
- Improve the AI chat page UI
### Bug Fixes
- Optimized the workspace menu loading performance
- Optimized tab switching performance
- Fixed searching issues in Document page
## Version 0.7.3 - 07/11/2024
### New Features
- Enable custom URLs for published pages
- Support toggling headings
- Create a subpage by typing in the document
- Turn selected blocks into a subpage
- Add a manual date picker for the Date property
### Bug Fixes
- Fixed an issue where the workspace owner was unable to delete spaces created by others
- Fixed cursor height inconsistencies with text height
- Fixed editing issues in Kanban cards
- Fixed an issue preventing images or files from being dropped into empty paragraphs
## Version 0.7.2 - 22/10/2024
### New Features
- Copy link to block
- Support turn into in document
- Enable sharing links and publishing pages on mobile
- Enable drag and drop in row documents
- Right-click on page in sidebar to open more actions
- Create new subpage in document using `+` character
- Allow reordering checklist item
### Bug Fixes
- Fixed issue with inability to cancel inline code format in French IME
- Fixed delete with Shift or Ctrl shortcuts not working in documents
- Fixed the issues with incorrect time zone being used in filters.
## Version 0.7.1 - 07/10/2024
### New Features
- Copy link to share and open it in a browser
- Enable the ability to edit the page title within the body of the document
- Filter by last modified, created at, or a date range
- Allow customization of database property icons
- Support CTRL/CMD+X to delete the current line when the selection is collapsed in the document
- Support window tiling on macOS
- Add filters to grid views on mobile
- Create and manage workspaces on mobile
- Automatically convert property types for imported CSV files
### Bug Fixes
- Fixed calculations with filters applied
- Fixed issues with importing data folders into a cloud account
- Fixed French IME backtick issues
- Fixed selection gesture bugs on mobile
## 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'
@@ -16,7 +109,7 @@
- Fixed the inability to edit group names on Kanban boards
- Made error codes more user-friendly
- Added leading zeros to day and month in date format
## Version 0.6.8 - 22/08/2024
### New Features
- Enabled viewing data inside a database record on mobile.
+49 -18
View File
@@ -42,10 +42,12 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
## User Installation
- [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/)
- 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)
@@ -61,15 +63,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
## Getting Started with development
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific
development instructions
## Roadmap
- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
If you'd like to propose a feature, submit a feature
request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit a bug
report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
## **Releases**
@@ -77,16 +82,24 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
## Contributing
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details.
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make
are **greatly appreciated**. Please look
at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy)
for details.
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly
easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains
the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with
us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
## Translations 🌎🗺
[![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use
the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or
run `npx inlang machine translate` to add missing translations.
## Join the community to build AppFlowy together
@@ -96,16 +109,30 @@ To add translations, you can manually edit the JSON translation files in `/front
## Why Are We Building This?
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and
functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations.
These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative
workplace management tools also have their constraints.
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market.
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have
led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates
from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a
proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to
come up with a one-size fits all solution in such a fragmented market.
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well.
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up,
in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is
a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention
the speed and native experience. The same may apply to individual users as well.
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs
well.
- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to
enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy
your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term
maintainability.
We decided to achieve this mission by upholding the three most fundamental values:
@@ -113,16 +140,20 @@ We decided to achieve this mission by upholding the three most fundamental value
- Reliable native experience
- Community-driven extensibility
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority
doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the
knowledge and wheels of making complex workplace management tools while enabling people and businesses to create
beautiful things on their own by equipping them with a versatile toolbox of building blocks.
## License
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information.
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for
more information.
## Acknowledgements
## Acknowledgments
Special thanks to these amazing projects which help power AppFlowy.IO:
- [flutter-quill](https://github.com/singerdmx/flutter-quill)
- [cargo-make](https://github.com/sagiegurari/cargo-make)
- [contrib.rocks](https://contrib.rocks)
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
+47
View File
@@ -0,0 +1,47 @@
workflows:
ios-workflow:
name: iOS Workflow
instance_type: mac_mini_m2
max_build_duration: 30
environment:
flutter: 3.22.3
xcode: latest
cocoapods: default
scripts:
- name: Build Flutter
script: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
rustc --version
cargo --version
cd frontend
rustup target install aarch64-apple-ios-sim
cargo install --force cargo-make
cargo install --force duckscript_cli
cargo install --force cargo-lipo
cargo make appflowy-flutter-deps-tools
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
cargo make --profile development-ios-arm64-sim code_generation
- name: iOS integration tests
script: |
cd frontend/appflowy_flutter
flutter emulators --launch apple_ios_simulator
flutter -d iPhone test integration_test/runner.dart
artifacts:
- build/ios/ipa/*.ipa
- /tmp/xcodebuild_logs/*.log
- flutter_drive.log
publishing:
email:
recipients:
- lucas.xu@appflowy.io
notify:
success: true
failure: true
+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.6"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -1,13 +1,3 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
@@ -16,17 +6,6 @@ analyzer:
- "**/*.freezed.dart"
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
- require_trailing_commas
@@ -51,8 +30,5 @@ linter:
- sort_constructors_first
- unawaited_futures
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
errors:
invalid_annotation_target: ignore
@@ -65,4 +65,5 @@
-->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.CAMERA" />
</manifest>
@@ -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,92 +0,0 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'dart:ui';
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/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/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.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:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
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';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('appflowy cloud', () {
testWidgets('anon user and then sign in', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
tester.expectToSeeText(LocaleKeys.signIn_loginStartWithAnonymous.tr());
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// rename the name of the anon user
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
await tester.pumpAndSettle();
await tester.enterUserName('local_user');
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(AccountSignInOutButton));
// sign up with Google
await tester.tapGoogleLoginInButton();
// sign out
await tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.logout();
await tester.pumpAndSettle();
// tap the continue as anonymous button
await tester
.tapButton(find.text(LocaleKeys.signIn_loginStartWithAnonymous.tr()));
await tester.expectToSeeHomePage();
// New anon user name
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
final userNameInput =
tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile;
expect(userNameInput.name, 'Me');
});
});
}
@@ -1,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,13 @@
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:flutter/material.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 '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
@@ -77,65 +77,46 @@ 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);
});
});
}
extension FlowySvgFinder on CommonFinders {
Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg);
}
class _FlowySvgFinder extends MatchFinder {
_FlowySvgFinder(this.svg);
final FlowySvgData svg;
@override
String get description => 'flowy_svg "$svg"';
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
return widget is FlowySvg && widget.svg == svg;
}
}
@@ -6,6 +6,7 @@ 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:time/time.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
@@ -14,6 +15,31 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board row test', () {
testWidgets('edit item in ToDo card', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
const name = 'Card 1';
final card1 = find.ancestor(
matching: find.byType(RowCard),
of: find.text(name),
);
await tester.hoverOnWidget(
card1,
onHover: () async {
final editCard = find.byType(EditCardAccessory);
await tester.tapButton(editCard);
},
);
await tester.showKeyboard(card1);
tester.testTextInput.enterText("");
await tester.pump(300.milliseconds);
tester.testTextInput.enterText("a");
await tester.pump(300.milliseconds);
expect(find.text('a'), findsOneWidget);
});
testWidgets('delete item in ToDo card', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -29,7 +55,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 +77,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,25 @@
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;
import 'set_env.dart' as preset_af_cloud_env_test;
Future<void> main() async {
preset_af_cloud_env_test.main();
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();
}
@@ -0,0 +1,57 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('appflowy cloud', () {
testWidgets('anon user -> sign in -> open imported space', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
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);
await tester.pumpAndSettle();
await tester.enterUserName('local_user');
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(AccountSignInOutButton));
// sign up with Google
await tester.tapGoogleLoginInButton();
// await tester.pumpAndSettle(const Duration(seconds: 16));
// open the imported space
await tester.expectToSeeHomePage();
await tester.clickSpaceHeader();
// 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.pumpAndSettle();
});
});
}
@@ -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,47 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.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('AI Writer:', () {
testWidgets('the ai writer transaction should only apply in memory',
(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,
);
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_aiWriter.tr(),
);
expect(find.byType(AIWriterBlockComponent), findsOneWidget);
// switch to another page
await tester.openPage(Constants.gettingStartedPageName);
// switch back to the page
await tester.openPage(pageName);
// expect the ai writer block is not in the document
expect(find.byType(AIWriterBlockComponent), findsNothing);
});
});
}
@@ -0,0 +1,275 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/shared/patterns/common_patterns.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/material.dart';
import 'package:flutter/services.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();
// copy link to block
group('copy link to block:', () {
testWidgets('copy link to check if the clipboard has the correct content',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
await tester.editor.copyLinkToBlock([0]);
await tester.pumpAndSettle(Durations.short1);
// check the clipboard
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text,
matches(appflowySharePageLinkPattern),
);
});
testWidgets('copy link to block(another page) and paste it in doc',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
await tester.editor.copyLinkToBlock([0]);
// create a new page and paste it
const pageName = 'copy link to block';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// paste the link to the new page
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.paste();
await tester.pumpAndSettle();
// check the content of the block
final node = tester.editor.getNodeAtPath([0]);
final delta = node.delta!;
final insert = (delta.first as TextInsert).text;
final attributes = delta.first.attributes;
expect(insert, MentionBlockKeys.mentionChar);
final mention =
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
expect(mention[MentionBlockKeys.type], MentionType.page.name);
expect(mention[MentionBlockKeys.blockId], isNotNull);
expect(mention[MentionBlockKeys.pageId], isNotNull);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
Constants.gettingStartedPageName,
findRichText: true,
),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
// the pasted block content is 'Welcome to AppFlowy'
'Welcome to AppFlowy',
findRichText: true,
),
),
findsOneWidget,
);
// tap the mention block to jump to the page
await tester.tapButton(find.byType(MentionPageBlock));
await tester.pumpAndSettle();
// expect to go to the getting started page
final documentPage = find.byType(DocumentPage);
expect(documentPage, findsOneWidget);
expect(
tester.widget<DocumentPage>(documentPage).view.name,
Constants.gettingStartedPageName,
);
// and the block is selected
expect(
tester.widget<DocumentPage>(documentPage).initialBlockId,
mention[MentionBlockKeys.blockId],
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection.collapsed(
Position(
path: [0],
),
),
);
});
testWidgets('copy link to block(same page) and paste it in doc',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// create a new page and paste it
const pageName = 'copy link to block';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// copy the link to block from the first line
const inputText = 'Hello World';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(inputText);
await tester.ime.insertCharacter('\n');
await tester.pumpAndSettle();
await tester.editor.copyLinkToBlock([0]);
// paste the link to the second line
await tester.editor.tapLineOfEditorAt(1);
await tester.editor.paste();
await tester.pumpAndSettle();
// check the content of the block
final node = tester.editor.getNodeAtPath([1]);
final delta = node.delta!;
final insert = (delta.first as TextInsert).text;
final attributes = delta.first.attributes;
expect(insert, MentionBlockKeys.mentionChar);
final mention =
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
expect(mention[MentionBlockKeys.type], MentionType.page.name);
expect(mention[MentionBlockKeys.blockId], isNotNull);
expect(mention[MentionBlockKeys.pageId], isNotNull);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
inputText,
findRichText: true,
),
),
findsNWidgets(2),
);
// edit the pasted block
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('!');
await tester.pumpAndSettle();
// check the content of the block
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
'$inputText!',
findRichText: true,
),
),
findsNWidgets(2),
);
// tap the mention block
await tester.tapButton(find.byType(MentionPageBlock));
expect(
tester.editor.getCurrentEditorState().selection,
Selection.collapsed(
Position(
path: [0],
),
),
);
});
testWidgets('''1. copy link to block from another page
2. paste the link to the new page
3. delete the original page
4. check the content of the block, it should be no access to the page
''', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
await tester.editor.copyLinkToBlock([0]);
// create a new page and paste it
const pageName = 'copy link to block';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// paste the link to the new page
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.paste();
await tester.pumpAndSettle();
// tap the mention block to jump to the page
await tester.tapButton(find.byType(MentionPageBlock));
await tester.pumpAndSettle();
// expect to go to the getting started page
final documentPage = find.byType(DocumentPage);
expect(documentPage, findsOneWidget);
expect(
tester.widget<DocumentPage>(documentPage).view.name,
Constants.gettingStartedPageName,
);
// delete the getting started page
await tester.hoverOnPageName(
Constants.gettingStartedPageName,
onHover: () async => tester.tapDeletePageButton(),
);
tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStarted);
// delete the page permanently
await tester.tapDeletePermanentlyButton();
// go back the page
await tester.openPage(pageName);
await tester.pumpAndSettle();
// check the content of the block
// it should be no access to the page
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.findTextInFlowyText(
LocaleKeys.document_mention_noAccess.tr(),
),
),
findsOneWidget,
);
});
});
}
@@ -0,0 +1,114 @@
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);
});
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,220 @@
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/plugins/shared/share/publish_tab.dart';
import 'package:appflowy/plugins/shared/share/share_menu.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/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Publish:', () {
testWidgets('publish document', (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,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
final publishButton = find.byType(PublishButton);
final unpublishButton = find.byType(UnPublishButton);
await tester.tapButton(publishButton);
// expect to see unpublish, visit site and manage all sites button
expect(unpublishButton, findsOneWidget);
expect(find.text(LocaleKeys.shareAction_visitSite.tr()), findsOneWidget);
// unpublish the document
await tester.tapButton(unpublishButton);
// expect to see publish button
expect(publishButton, findsOneWidget);
});
testWidgets('rename path name', (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,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
final publishButton = find.byType(PublishButton);
await tester.tapButton(publishButton);
// rename the path name
final inputField = find.descendant(
of: find.byType(ShareMenu),
matching: find.byType(TextField),
);
// rename with invalid name
await tester.tap(inputField);
await tester.enterText(inputField, '&&&&????');
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with error message
final errorToast1 = find.text(
LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters
.tr(),
);
await tester.pumpUntilFound(errorToast1);
await tester.pumpUntilNotFound(errorToast1);
// rename with long name
await tester.tap(inputField);
await tester.enterText(inputField, 'long-path-name' * 200);
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with error message
final errorToast2 = find.text(
LocaleKeys.settings_sites_error_publishNameTooLong.tr(),
);
await tester.pumpUntilFound(errorToast2);
await tester.pumpUntilNotFound(errorToast2);
// rename with empty name
await tester.tap(inputField);
await tester.enterText(inputField, '');
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with error message
final errorToast3 = find.text(
LocaleKeys.settings_sites_error_publishNameCannotBeEmpty.tr(),
);
await tester.pumpUntilFound(errorToast3);
await tester.pumpUntilNotFound(errorToast3);
// input the new path name
await tester.tap(inputField);
await tester.enterText(inputField, 'new-path-name');
// click save button
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with success message
final successToast = find.text(
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
await tester.pumpUntilFound(successToast);
await tester.pumpUntilNotFound(successToast);
// click the copy link button
await tester.tapButton(
find.byWidgetPredicate(
(widget) =>
widget is FlowySvg &&
widget.svg.path == FlowySvgs.m_toolbar_link_m.path,
),
);
await tester.pumpAndSettle();
// check the clipboard has the link
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text?.contains('new-path-name'),
isTrue,
);
});
testWidgets('re-publish the document', (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,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
final publishButton = find.byType(PublishButton);
await tester.tapButton(publishButton);
// rename the path name
final inputField = find.descendant(
of: find.byType(ShareMenu),
matching: find.byType(TextField),
);
// input the new path name
const newName = 'new-path-name';
await tester.enterText(inputField, newName);
// click save button
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with success message
final successToast = find.text(
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
await tester.pumpUntilNotFound(successToast);
// unpublish the document
final unpublishButton = find.byType(UnPublishButton);
await tester.tapButton(unpublishButton);
final unpublishSuccessToast = find.text(
LocaleKeys.publish_unpublishSuccessfully.tr(),
);
await tester.pumpUntilNotFound(unpublishSuccessToast);
// re-publish the document
await tester.tapButton(publishButton);
// expect to see the toast with success message
final rePublishSuccessToast = find.text(
LocaleKeys.publish_publishSuccessfully.tr(),
);
await tester.pumpUntilNotFound(rePublishSuccessToast);
// check the clipboard has the link
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text?.contains(newName),
isTrue,
);
});
});
}
@@ -0,0 +1,16 @@
import 'package:integration_test/integration_test.dart';
import 'document_ai_writer_test.dart' as document_ai_writer_test;
import 'document_copy_link_to_block_test.dart'
as document_copy_link_to_block_test;
import 'document_option_actions_test.dart' as document_option_actions_test;
import 'document_publish_test.dart' as document_publish_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
document_option_actions_test.main();
document_copy_link_to_block_test.main();
document_publish_test.main();
document_ai_writer_test.main();
}
@@ -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,108 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.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';
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();
}
// Attempt right-click on the page name and expect not to see
await tester.tap(gettingStarted.last, buttons: kSecondaryButton);
await tester.pumpAndSettle();
expect(
find.text(LocaleKeys.disclosureAction_moveTo.tr()),
findsOneWidget,
);
// 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);
});
});
}
@@ -1,22 +1,13 @@
// ignore_for_file: unused_import
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.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/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -86,10 +77,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 +91,7 @@ void main() {
// the switch should be on after toggling
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
await tester.wait(250);
});
});
}
@@ -1,25 +1,10 @@
// 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/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/settings/pages/settings_account_view.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/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -0,0 +1,7 @@
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'user_setting_sync_test.dart' as user_sync_test;
void main() async {
appflowy_cloud_auth_test.main();
user_sync_test.main();
}
@@ -1,33 +1,11 @@
// 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/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/settings/pages/account/account_user_profile.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.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/style_widget/text_field.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 '../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/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -1,23 +1,14 @@
// ignore_for_file: unused_import
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.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: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/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../../shared/workspace.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -49,6 +40,10 @@ void main() {
await tester.changeWorkspaceIcon(icon);
await tester.changeWorkspaceName(name);
await tester.pumpUntilNotFound(
find.text(LocaleKeys.workspace_renameSuccess.tr()),
);
workspaceIcon = tester.widget<WorkspaceIcon>(
find.byType(WorkspaceIcon),
);
@@ -0,0 +1,152 @@
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/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:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.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);
},
);
});
testWidgets('check the member count immediately after creating a workspace',
(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);
await tester.openCollaborativeWorkspaceMenu();
// expect to see the member count
final memberCount = find.text('1 member');
expect(memberCount, findsNWidgets(2));
});
testWidgets('only display one menu item in the workspace menu',
(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);
await tester.openCollaborativeWorkspaceMenu();
// hover on the workspace and click the more button
final workspaceItem = find.byWidgetPredicate(
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
);
await tester.hoverOnWidget(
workspaceItem,
onHover: () async {
final moreButton = find.byWidgetPredicate(
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
);
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
// click it again
await tester.tapButton(moreButton);
// nothing should happen
expect(
find.text(LocaleKeys.button_rename.tr()),
findsOneWidget,
);
},
);
});
});
}
@@ -0,0 +1,65 @@
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/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.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('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,
matches(appflowySharePageLinkPattern),
);
final shareValues = plainText!
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, isNotEmpty);
final pageId = shareValues[1];
expect(pageId, isNotEmpty);
});
});
}
@@ -0,0 +1,75 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
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/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tabs', () {
testWidgets('close other tabs before opening a new workspace',
(tester) async {
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);
// create new tabs in the workspace
expect(find.byType(FlowyTab), findsNothing);
const documentOneName = 'document one';
const documentTwoName = 'document two';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: documentOneName,
);
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: documentTwoName,
);
/// Open second menu item in a new tab
await tester.openAppInNewTab(documentOneName, ViewLayoutPB.Document);
/// Open third menu item in a new tab
await tester.openAppInNewTab(documentTwoName, ViewLayoutPB.Document);
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(2),
);
// switch to the another workspace
final Finder items = find.byType(WorkspaceMenuItem);
await tester.openCollaborativeWorkspaceMenu();
await tester.pumpUntilFound(items);
expect(items, findsNWidgets(2));
// open the first workspace
await tester.tap(items.first);
await tester.pumpUntilNotFound(loading);
expect(find.byType(FlowyTab), findsNothing);
});
});
}
@@ -0,0 +1,44 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.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';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('workspace icon:', () {
testWidgets('remove icon from workspace', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openWorkspaceMenu();
// click the workspace icon
await tester.tapButton(
find.descendant(
of: find.byType(WorkspaceMenuItem),
matching: find.byType(WorkspaceIcon),
),
);
// click the remove icon button
await tester.tapButton(
find.text(LocaleKeys.button_remove.tr()),
);
// nothing should happen
expect(
find.text(LocaleKeys.workspace_updateIconSuccess.tr()),
findsNothing,
);
});
});
}
@@ -0,0 +1,345 @@
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/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/shared/share/publish_tab.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.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_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/constants.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,
);
},
);
});
group('sites settings:', () {
testWidgets(
'manage published page, set it as homepage, remove the homepage',
(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,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
await tester.tapButton(find.byType(PublishButton));
// click empty area to close the publish menu
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
// check if the page is published in sites page
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
// check if the page is published in sites page
final pageItem = find.byWidgetPredicate(
(widget) =>
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
expect(pageItem, findsOneWidget);
// comment it out because it's not allowed to update the namespace in free plan
// // set it to homepage
// await tester.tapButton(
// find.textContaining(
// LocaleKeys.settings_sites_selectHomePage.tr(),
// ),
// );
// await tester.tapButton(
// find.descendant(
// of: find.byType(SelectHomePageMenu),
// matching: find.text(pageName),
// ),
// );
// await tester.pumpAndSettle();
// // check if the page is set to homepage
// final homePageItem = find.descendant(
// of: find.byType(DomainItem),
// matching: find.text(pageName),
// );
// expect(homePageItem, findsOneWidget);
// // remove the homepage
// await tester.tapButton(find.byType(DomainMoreAction));
// await tester.tapButton(
// find.text(LocaleKeys.settings_sites_removeHomepage.tr()),
// );
// await tester.pumpAndSettle();
// // check if the page is removed from homepage
// expect(homePageItem, findsNothing);
});
testWidgets('update namespace', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// check if the page is published in sites page
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
// update the domain
final domainMoreAction = find.byType(DomainMoreAction);
await tester.tapButton(domainMoreAction);
final updateNamespaceButton = find.text(
LocaleKeys.settings_sites_updateNamespace.tr(),
);
await tester.pumpUntilFound(updateNamespaceButton);
// click the update namespace button
await tester.tapButton(updateNamespaceButton);
// comment it out because it's not allowed to update the namespace in free plan
// expect to see the dialog
// await tester.updateNamespace('&&&???');
// // need to upgrade to pro plan to update the namespace
// final errorToast = find.text(
// LocaleKeys.settings_sites_error_proPlanLimitation.tr(),
// );
// await tester.pumpUntilFound(errorToast);
// expect(errorToast, findsOneWidget);
// await tester.pumpUntilNotFound(errorToast);
// comment it out because it's not allowed to update the namespace in free plan
// // short namespace
// await tester.updateNamespace('a');
// // expect to see the toast with error message
// final errorToast2 = find.text(
// LocaleKeys.settings_sites_error_namespaceTooShort.tr(),
// );
// await tester.pumpUntilFound(errorToast2);
// expect(errorToast2, findsOneWidget);
// await tester.pumpUntilNotFound(errorToast2);
// // valid namespace
// await tester.updateNamespace('AppFlowy');
// // expect to see the toast with success message
// final successToast = find.text(
// LocaleKeys.settings_sites_success_namespaceUpdated.tr(),
// );
// await tester.pumpUntilFound(successToast);
// expect(successToast, findsOneWidget);
});
testWidgets('''
More actions for published page:
1. visit site
2. copy link
3. settings
4. unpublish
5. custom url
''', (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,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
await tester.tapButton(find.byType(PublishButton));
// click empty area to close the publish menu
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
// check if the page is published in sites page
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
// check if the page is published in sites page
final pageItem = find.byWidgetPredicate(
(widget) =>
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
expect(pageItem, findsOneWidget);
final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());
final customUrlItem = find.text(LocaleKeys.settings_sites_customUrl.tr());
final unpublishItem = find.text(LocaleKeys.shareAction_unPublish.tr());
// custom url
final publishMoreAction = find.byType(PublishedViewMoreAction);
// click the copy link button
{
await tester.tapButton(publishMoreAction);
await tester.pumpAndSettle();
await tester.pumpUntilFound(copyLinkItem);
await tester.tapButton(copyLinkItem);
await tester.pumpAndSettle();
await tester.pumpUntilNotFound(copyLinkItem);
final clipboardContent = await getIt<ClipboardService>().getData();
final plainText = clipboardContent.plainText;
expect(
plainText,
contains(pageName),
);
}
// custom url
{
await tester.tapButton(publishMoreAction);
await tester.pumpAndSettle();
await tester.pumpUntilFound(customUrlItem);
await tester.tapButton(customUrlItem);
await tester.pumpAndSettle();
await tester.pumpUntilNotFound(customUrlItem);
// see the custom url dialog
final customUrlDialog = find.byType(PublishedViewSettingsDialog);
expect(customUrlDialog, findsOneWidget);
// rename the custom url
final textField = find.descendant(
of: customUrlDialog,
matching: find.byType(TextField),
);
await tester.enterText(textField, 'hello-world');
await tester.pumpAndSettle();
// click the save button
final saveButton = find.descendant(
of: customUrlDialog,
matching: find.text(LocaleKeys.button_save.tr()),
);
await tester.tapButton(saveButton);
await tester.pumpAndSettle();
// expect to see the toast with success message
final successToast = find.text(
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
await tester.pumpUntilFound(successToast);
expect(successToast, findsOneWidget);
}
// unpublish
{
await tester.tapButton(publishMoreAction);
await tester.pumpAndSettle();
await tester.pumpUntilFound(unpublishItem);
await tester.tapButton(unpublishItem);
await tester.pumpAndSettle();
await tester.pumpUntilNotFound(unpublishItem);
// expect to see the toast with success message
final successToast = find.text(
LocaleKeys.publish_unpublishSuccessfully.tr(),
);
await tester.pumpUntilFound(successToast);
expect(successToast, findsOneWidget);
await tester.pumpUntilNotFound(successToast);
// check if the page is unpublished in sites page
expect(pageItem, findsNothing);
}
});
});
}
@@ -0,0 +1,19 @@
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 'tabs_test.dart' as tabs_test;
import 'workspace_icon_test.dart' as workspace_icon_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();
workspace_icon_test.main();
tabs_test.main();
}
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.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:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -277,5 +277,72 @@ void main() {
tester.assertRowDetailPageOpened();
});
testWidgets('filter calendar events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// Create the calendar view
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Calendar,
);
// Create a new event on the first of this month
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissEventEditor();
tester.assertNumberOfEventsInCalendar(1);
await tester.openCalendarEvent(index: 0, date: firstOfThisMonth);
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
await tester.createOption(name: "asdf");
await tester.createOption(name: "qwer");
await tester.selectOption(name: "asdf");
await tester.dismissCellEditor();
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, "Tags");
await tester.tapFilterButtonInGrid('Tags');
await tester.tapOptionFilterWithName('asdf');
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(0);
await tester.tapFilterButtonInGrid('Tags');
await tester.tapOptionFilterWithName('asdf');
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(1);
await tester.tapFilterButtonInGrid('Tags');
await tester.tapOptionFilterWithName('asdf');
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(0);
final secondOfThisMonth = DateTime(today.year, today.month, 2);
await tester.doubleClickCalendarCell(secondOfThisMonth);
await tester.dismissEventEditor();
tester.assertNumberOfEventsInCalendar(1);
await tester.openCalendarEvent(index: 0, date: secondOfThisMonth);
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
await tester.selectOption(name: "asdf");
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(0);
await tester.tapFilterButtonInGrid('Tags');
await tester.changeSelectFilterCondition(
SelectOptionFilterConditionPB.OptionIsEmpty,
);
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, secondOfThisMonth);
});
});
}
@@ -211,21 +211,21 @@ void main() {
await tester.toggleIncludeTime();
// Select a date
final today = DateTime.now();
await tester.selectDay(content: today.day);
DateTime 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();
now = DateTime.now();
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");
});
});
}
@@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.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-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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';
@@ -15,7 +16,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 +25,6 @@ void main() {
// Invoke the field editor
await tester.tapGridFieldWithName('Name');
await tester.tapEditFieldButton();
await tester.renameField('hello world');
await tester.dismissFieldEditor();
@@ -33,6 +33,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 +152,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 +271,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));
@@ -292,6 +317,30 @@ void main() {
);
});
testWidgets('text in viewport while typing', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.changeCalculateAtIndex(0, CalculationType.Count);
// add very large text with 200 lines
final largeText = List.generate(
200,
(index) => 'Line ${index + 1}',
).join('\n');
await tester.editCell(
rowIndex: 2,
fieldType: FieldType.RichText,
input: largeText,
);
// checks if last line is in view port
tester.expectToSeeText('Line 200');
});
// Disable this test because it fails on CI randomly
// testWidgets('last modified and created at field type options',
// (tester) async {
@@ -357,5 +406,188 @@ 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),
);
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/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/plugins/database/widgets/row/row_detail.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(2);
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,95 @@ 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.changeDateFilterCondition(DateTimeFilterCondition.before);
tester.assertNumberOfRowsInGridPage(7);
await tester.changeDateFilterCondition(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.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.changeDateFilterCondition(DateTimeFilterCondition.before);
tester.assertNumberOfRowsInGridPage(0);
await tester.pumpAndSettle();
});
testWidgets('create new row when filters don\'t autofill', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.RichText,
'Name',
);
tester.assertNumberOfRowsInGridPage(3);
await tester.tapCreateRowButtonInGrid();
tester.assertNumberOfRowsInGridPage(4);
await tester.tapFilterButtonInGrid('Name');
await tester
.changeTextFilterCondition(TextFilterConditionPB.TextIsNotEmpty);
await tester.dismissCellEditor();
tester.assertNumberOfRowsInGridPage(0);
await tester.tapCreateRowButtonInGrid();
tester.assertNumberOfRowsInGridPage(0);
expect(find.byType(RowDetailPage), findsOneWidget);
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';
@@ -23,7 +26,7 @@ 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();
@@ -65,10 +68,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
await tester.tapFileUploadHint();
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
@@ -81,9 +81,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.tapFileUploadHint();
await tester.pumpAndSettle();
// Expect two files
@@ -92,5 +90,208 @@ 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.tapFileUploadHint();
// 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.tapFileUploadHint();
// 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('show 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.tapFileUploadHint();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
await tester.dismissCellEditor();
await tester.pumpAndSettle();
// Open first row in row detail view then toggle show file names
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to not be shown (hidden)
expect(find.text('sample.jpeg'), findsNothing);
expect(find.text('sample.gif'), findsNothing);
await tester.tapGridFieldWithNameInRowDetailPage('Type');
await tester.pumpAndSettle();
// Toggle show file names
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
// Expect file names to be shown
expect(find.text('sample.jpeg'), findsOneWidget);
expect(find.text('sample.gif'), findsOneWidget);
await tester.dismissRowDetailPage();
await tester.pumpAndSettle();
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
});
}
@@ -0,0 +1,131 @@
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/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-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 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,13 +1,18 @@
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/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.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_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:flowy_infra_ui/style_widget/text.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -36,26 +41,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 +54,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
@@ -80,6 +76,24 @@ void main() {
// The number of emoji should be two. One in the row displayed in the grid
// one in the row detail page.
expect(emojiText, findsNWidgets(2));
// insert a sub page in database
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_subPage_name.tr(),
offset: 100,
);
await tester.pumpAndSettle();
// the row detail page should be closed
final rowDetailPage = find.byType(RowDetailPage);
await tester.pumpUntilNotFound(rowDetailPage);
// expect to see a document page
final documentPage = find.byType(DocumentPage);
expect(documentPage, findsOneWidget);
});
testWidgets('remove emoji', (tester) async {
@@ -96,7 +110,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 +337,7 @@ void main() {
await tester.tapRowDetailPageDeleteRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(2);
tester.assertNumberOfRowsInGridPage(2);
});
testWidgets('duplicate row', (tester) async {
@@ -338,7 +354,141 @@ void main() {
await tester.tapRowDetailPageDuplicateRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(4);
tester.assertNumberOfRowsInGridPage(4);
});
testWidgets('edit checklist cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.Checklist;
await tester.createField(fieldType);
await tester.openFirstRowDetailPage();
await tester.hoverOnWidget(
find.byType(ChecklistRowDetailCell),
onHover: () async {
await tester.tapButton(find.byType(ChecklistItemControl));
},
);
tester.assertPhantomChecklistItemAtIndex(index: 0);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 1');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 1);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 2);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(find.byType(PhantomChecklistItem), findsNothing);
await tester.renameChecklistTask(index: 0, name: "task -1", enter: false);
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
tester.assertChecklistTaskInEditor(
index: 0,
name: "task -1",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 1);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 0');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertPhantomChecklistItemAtIndex(index: 2);
await tester.checkChecklistTask(index: 1);
expect(find.byType(PhantomChecklistItem), findsNothing);
expect(find.byType(ChecklistItem), findsNWidgets(3));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task -1",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 0",
isChecked: true,
);
tester.assertChecklistTaskInEditor(
index: 2,
name: "task 2",
isChecked: false,
);
await tester.tapButton(
find.descendant(
of: find.byType(ProgressAndHideCompleteButton),
matching: find.byType(FlowyIconButton),
),
);
expect(find.byType(ChecklistItem), findsNWidgets(2));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task -1",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
await tester.renameChecklistTask(index: 1, name: "task 3", enter: false);
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.renameChecklistTask(index: 0, name: "task 1", enter: false);
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 2,
name: "task 3",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 2);
await tester.checkChecklistTask(index: 1);
expect(find.byType(ChecklistItem), findsNWidgets(2));
});
});
}
@@ -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,20 @@
import 'package:integration_test/integration_test.dart';
import 'database_cell_test.dart' as database_cell_test;
import 'database_field_settings_test.dart' as database_field_settings_test;
import 'database_field_test.dart' as database_field_test;
import 'database_row_page_test.dart' as database_row_page_test;
import 'database_setting_test.dart' as database_setting_test;
import 'database_share_test.dart' as database_share_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_cell_test.main();
database_field_test.main();
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_setting_test.main();
// DON'T add more tests here.
}
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_media_test.dart' as database_media_test;
import 'database_row_cover_test.dart' as database_row_cover_test;
import 'database_share_test.dart' as database_share_test;
import 'database_sort_test.dart' as database_sort_test;
import 'database_view_test.dart' as database_view_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
database_media_test.main();
database_row_cover_test.main();
database_share_test.main();
// DON'T add more tests here.
}
@@ -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);
},
);
});
}
@@ -0,0 +1,47 @@
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() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Block option interaction tests', () {
testWidgets('has correct block selection on tap option button',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// We edit the document by entering some characters, to ensure the document has focus
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [2])),
);
// Insert character 'a' three times - easy to identify
await tester.ime.insertText('aaa');
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([2]);
expect(node?.delta?.toPlainText(), startsWith('aaa'));
final multiSelection = Selection(
start: Position(path: [2], offset: 3),
end: Position(path: [4], offset: 40),
);
// Select multiple items
await tester.editor.updateSelection(multiSelection);
await tester.pumpAndSettle();
// Press the block option menu
await tester.editor.hoverAndClickOptionMenuButton([2]);
await tester.pumpAndSettle();
// Expect the selection to be Block type and not have changed
expect(editorState.selectionType, SelectionType.block);
expect(editorState.selection, multiSelection);
});
});
}
@@ -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,84 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.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';
import 'document_inline_page_reference_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Document deletion', () {
testWidgets('Trash breadcrumb', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// This test shares behavior with the inline page reference test, thus
// we utilize the same helper functions there.
final name = await createDocumentToReference(tester);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await triggerReferenceDocumentBySlashMenu(tester);
// Search for prefix of document
await enterDocumentText(tester);
// Select result
final optionFinder = find.descendant(
of: find.byType(InlineActionsHandler),
matching: find.text(name),
);
await tester.tap(optionFinder);
await tester.pumpAndSettle();
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
// Delete the page
await tester.hoverOnPageName(
name,
onHover: () async => tester.tapDeletePageButton(),
);
await tester.pumpAndSettle();
// Navigate to the deleted page from the inline mention
await tester.tap(mentionBlock);
await tester.pumpUntilFound(find.byType(TrashBreadcrumb));
expect(find.byType(TrashBreadcrumb), findsOneWidget);
// Navigate using the trash breadcrumb
await tester.tap(
find.descendant(
of: find.byType(TrashBreadcrumb),
matching: find.text(
LocaleKeys.trash_text.tr(),
),
),
);
await tester.pumpUntilFound(find.text(LocaleKeys.trash_restoreAll.tr()));
// Restore all
await tester.tap(find.text(LocaleKeys.trash_restoreAll.tr()));
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.trash_restore.tr()));
await tester.pumpAndSettle();
// Navigate back to the document
await tester.openPage('Getting started');
await tester.pumpAndSettle();
await tester.tap(mentionBlock);
await tester.pumpAndSettle();
expect(find.byType(TrashBreadcrumb), findsNothing);
});
});
}
@@ -0,0 +1,144 @@
import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.dart';
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/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
String generateRandomString(int len) {
final r = Random();
return String.fromCharCodes(
List.generate(len, (index) => r.nextInt(33) + 89),
);
}
testWidgets(
'document find menu test',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent();
// tap editor to get focus
await tester.tapButton(find.byType(AppFlowyEditor));
// set clipboard data
final data = [
"123456\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"1234567\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"12345678\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
].join();
await getIt<ClipboardService>().setData(
ClipboardServiceData(
plainText: data,
),
);
// paste
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
// go back to beginning of document
// FIXME: Cannot run Ctrl+F unless selection is on screen
await tester.editor
.updateSelection(Selection.collapsed(Position(path: [0])));
await tester.pumpAndSettle();
expect(find.byType(FindAndReplaceMenuWidget), findsNothing);
// press cmd/ctrl+F to display the find menu
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyF,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget);
final textField = find.descendant(
of: find.byType(FindAndReplaceMenuWidget),
matching: find.byType(TextField),
);
await tester.enterText(
textField,
"123456",
);
await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.text("123456", findRichText: true),
),
findsOneWidget,
);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.text("1234567", findRichText: true),
),
findsOneWidget,
);
await tester.showKeyboard(textField);
await tester.idle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.text("12345678", findRichText: true),
),
findsOneWidget,
);
// tap next button, go back to beginning of document
await tester.tapButton(
find.descendant(
of: find.byType(FindMenu),
matching: find.byFlowySvg(FlowySvgs.arrow_down_s),
),
);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.text("123456", findRichText: true),
),
findsOneWidget,
);
},
);
}
@@ -0,0 +1,365 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
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/keyboard.dart';
import '../../shared/util.dart';
const _firstDocName = "Inline Sub Page Mention";
const _createdPageName = "hi world";
// Test cases that are covered in this file:
// - [x] Insert sub page mention from action menu (+)
// - [x] Delete sub page mention from editor
// - [x] Delete page from sidebar
// - [x] Delete page from sidebar and then trash
// - [x] Undo delete sub page mention
// - [x] Cut+paste in same document
// - [x] Cut+paste in different document
// - [x] Cut+paste in same document and then paste again in same document
// - [x] Turn paragraph with sub page mention into a heading
// - [x] Turn heading with sub page mention into a paragraph
// - [x] Duplicate a Block containing two sub page mentions
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document inline sub-page mention tests:', () {
testWidgets('Insert (& delete) a sub page mention from action menu',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Delete from editor
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
// Undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Move to trash (delete from sidebar)
await tester.rightClickOnPageName(_createdPageName);
await tester.tapButtonWithName(ViewMoreActionType.delete.name);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(
find.text(LocaleKeys.document_mention_trashHint.tr()),
findsOneWidget,
);
// Delete from trash
await tester.tapTrashButton();
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr()));
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.button_delete.tr()));
await tester.pumpAndSettle();
await tester.openPage(_firstDocName);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(
find.text(LocaleKeys.document_mention_deletedPage.tr()),
findsOneWidget,
);
});
testWidgets(
'Cut+paste in same document and cut+paste in different document',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Cut from editor
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
// Paste in same document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Cut again
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
// Create another document
const anotherDocName = "Another Document";
await tester.createOpenRenameDocumentUnderParent(
name: anotherDocName,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
// Paste in document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpUntilFound(find.byType(MentionSubPageBlock));
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
await tester.expandOrCollapsePage(
pageName: anotherDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
});
testWidgets(
'Cut+paste in same docuemnt and then paste again in same document',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Cut from editor
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
// Paste in same document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Paste again
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle(const Duration(seconds: 2));
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
expect(find.text('$_createdPageName (copy)'), findsNWidgets(2));
});
testWidgets('Turn into w/ sub page mentions', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
final headingText = LocaleKeys.document_slashMenu_name_heading1.tr();
final paragraphText = LocaleKeys.document_slashMenu_name_text.tr();
// Turn into heading
await tester.editor.openTurnIntoMenu([0]);
await tester.tapButton(find.findTextInFlowyText(headingText));
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Turn into paragraph
await tester.editor.openTurnIntoMenu([0]);
await tester.tapButton(find.findTextInFlowyText(paragraphText));
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
});
testWidgets('Duplicate a block containing two sub page mentions',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
// Copy paste it
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: 1),
),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
expect(find.text("$_createdPageName (copy)"), findsOneWidget);
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
// Duplicate node from block action menu
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
expect(find.text("$_createdPageName (copy)"), findsNWidgets(2));
expect(find.text("$_createdPageName (copy) (copy)"), findsOneWidget);
});
});
}
extension _InlineSubPageTestHelper on WidgetTester {
Future<void> insertInlineSubPageFromPlusMenu() async {
await editor.tapLineOfEditorAt(0);
await editor.showPlusMenu();
// Workaround to allow typing a document name
await FlowyTestKeyboard.simulateKeyDownEvent(
tester: this,
withKeyUp: true,
[
LogicalKeyboardKey.keyH,
LogicalKeyboardKey.keyI,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyW,
LogicalKeyboardKey.keyO,
LogicalKeyboardKey.keyR,
LogicalKeyboardKey.keyL,
LogicalKeyboardKey.keyD,
],
);
await FlowyTestKeyboard.simulateKeyDownEvent(
tester: this,
withKeyUp: true,
[LogicalKeyboardKey.enter],
);
await pumpUntilFound(find.byType(MentionSubPageBlock));
}
}
@@ -1,3 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
import 'package:appflowy_editor/appflowy_editor.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';
@@ -7,9 +12,23 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// +, ... button beside the block component.
group('document with option action button', () {
testWidgets(
'click + to add a block after current selection, and click + and option key to add a block before current selection',
group('block option action:', () {
Future<void> turnIntoBlock(
WidgetTester tester,
Path path, {
required String menuText,
required String afterType,
}) async {
await tester.editor.openTurnIntoMenu(path);
await tester.tapButton(
find.findTextInFlowyText(menuText),
);
final node = tester.editor.getCurrentEditorState().getNodeAtPath(path);
expect(node?.type, afterType);
}
testWidgets('''click + to add a block after current selection,
and click + and option key to add a block before current selection''',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -40,5 +59,122 @@ void main() {
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
});
testWidgets('turn into - single line', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const name = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: name);
await tester.openPage(name);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('turn into');
// click the block option button to convert it to another blocks
final values = {
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_bulletedList.tr():
BulletedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_numberedList.tr():
NumberedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
LocaleKeys.document_slashMenu_name_todoList.tr():
TodoListBlockKeys.type,
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
};
for (final value in values.entries) {
final menuText = value.key;
final afterType = value.value;
await turnIntoBlock(
tester,
[0],
menuText: menuText,
afterType: afterType,
);
}
});
testWidgets('turn into - multi lines', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const name = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: name);
await tester.openPage(name);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('turn into 1');
await tester.ime.insertCharacter('\n');
await tester.ime.insertText('turn into 2');
// click the block option button to convert it to another blocks
final values = {
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_bulletedList.tr():
BulletedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_numberedList.tr():
NumberedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
LocaleKeys.document_slashMenu_name_todoList.tr():
TodoListBlockKeys.type,
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
};
for (final value in values.entries) {
final editorState = tester.editor.getCurrentEditorState();
editorState.selection = Selection(
start: Position(path: [0]),
end: Position(path: [1], offset: 2),
);
final menuText = value.key;
final afterType = value.value;
await turnIntoBlock(
tester,
[0],
menuText: menuText,
afterType: afterType,
);
}
});
testWidgets(
'selecting the parent should deselect all the child nodes as well',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const name = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: name);
await tester.openPage(name);
// create a nested list
// Item 1
// Nested Item 1
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('Item 1');
await tester.ime.insertCharacter('\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.tab);
await tester.ime.insertText('Nested Item 1');
// select the 'Nested Item 1' and then tap the option button of the 'Item 1'
final editorState = tester.editor.getCurrentEditorState();
final selection = Selection.collapsed(
Position(path: [0, 0], offset: 1),
);
editorState.selection = selection;
await tester.pumpAndSettle();
expect(editorState.selection, selection);
await tester.editor.hoverAndClickOptionMenuButton([0]);
expect(editorState.selection, Selection.collapsed(Position(path: [0])));
},
);
});
}
@@ -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,538 @@
import 'dart:io';
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/workspace/presentation/home/menu/view/view_action_type.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/material.dart';
import 'package:flutter/services.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 deleted 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)
/// The defaut page name is empty, if we're looking for a "text" we can look for
/// [LocaleKeys.menuAppHeader_defaultNewPageName] but it won't work for eg. hoverOnPageName
/// as it looks at the text provided instead of the actual displayed text.
///
const _defaultPageName = "";
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.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
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.renamePageWithSecondary(_defaultPageName, 'Child page');
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.renamePageWithSecondary(_defaultPageName, 'Child page');
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.renamePageWithSecondary(_defaultPageName, 'Child page');
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.renamePageWithSecondary(_defaultPageName, 'Child page');
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 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.renamePageWithSecondary(_defaultPageName, 'Child page');
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);
// Since there is no selection active in editor before deleting Node,
// we need to give focus back to the editor
await tester.editor
.updateSelection(Selection.collapsed(Position(path: [0])));
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 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.renamePageWithSecondary(_defaultPageName, 'Child page');
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 from sidebar', (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.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
await tester.hoverOnPageName(
'Child page',
onHover: () async {
await tester.tapDeletePageButton();
},
);
await tester.pumpAndSettle(const Duration(seconds: 1));
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.renamePageWithSecondary(_defaultPageName, 'Child page');
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);
});
testWidgets('turn into page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
final editorState = tester.editor.getCurrentEditorState();
// Insert nested list
final transaction = editorState.transaction;
transaction.insertNode(
[0],
bulletedListNode(
text: 'Parent',
children: [
bulletedListNode(text: 'Child 1'),
bulletedListNode(text: 'Child 2'),
],
),
);
await editorState.apply(transaction);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButtonWithName(
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
);
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.editor_page.tr()));
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsOneWidget);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(find.text('Parent'), findsNWidgets(2));
});
});
}
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,
);
// Navigate to the previous page to see the SubPageBlock
await openPage('SubPageBlock');
await pumpAndSettle();
await pumpUntilFound(find.byType(SubPageBlockComponent));
}
Future<void> renamePageWithSecondary(
String currentName,
String newName,
) async {
await hoverOnPageName(currentName, onHover: () async => pumpAndSettle());
await rightClickOnPageName(currentName);
await tapButtonWithName(ViewMoreActionType.rename.name);
await enterText(find.byType(TextFormField), newName);
await tapOKButton();
await pumpAndSettle();
}
}
@@ -1,48 +0,0 @@
import 'package:integration_test/integration_test.dart';
import 'document_alignment_test.dart' as document_alignment_test;
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_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;
import 'document_with_file_test.dart' as document_with_file_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_create_and_delete_test.main();
document_edit_test.main();
document_with_database_test.main();
document_with_inline_page_test.main();
document_with_inline_math_equation_test.main();
document_with_cover_image_test.main();
document_with_outline_block.main();
document_with_toggle_list_test.main();
document_copy_and_paste_test.main();
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_test.main();
document_option_action_test.main();
document_with_image_block_test.main();
document_with_multi_image_block_test.main();
document_inline_page_reference_test.main();
document_more_actions_test.main();
document_with_file_test.main();
}
@@ -0,0 +1,23 @@
import 'package:integration_test/integration_test.dart';
import 'document_create_and_delete_test.dart'
as document_create_and_delete_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;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'edit_document_test.dart' as document_edit_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_create_and_delete_test.main();
document_edit_test.main();
document_with_database_test.main();
document_with_inline_page_test.main();
document_with_inline_math_equation_test.main();
document_with_cover_image_test.main();
// Don't add new tests here.
}
@@ -0,0 +1,26 @@
import 'package:integration_test/integration_test.dart';
import 'document_app_lifecycle_test.dart' as document_app_lifecycle_test;
import 'document_deletion_test.dart' as document_deletion_test;
import 'document_inline_sub_page_test.dart' as document_inline_sub_page_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_title_test.dart' as document_title_test;
import 'document_with_date_reminder_test.dart'
as document_with_date_reminder_test;
import 'document_with_toggle_heading_block_test.dart'
as document_with_toggle_heading_block_test;
import 'document_sub_page_test.dart' as document_sub_page_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_title_test.main();
document_app_lifecycle_test.main();
document_with_date_reminder_test.main();
document_deletion_test.main();
document_option_action_test.main();
document_inline_sub_page_test.main();
document_with_toggle_heading_block_test.main();
document_sub_page_test.main();
}
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'document_alignment_test.dart' as document_alignment_test;
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_text_direction_test.dart' as document_text_direction_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_with_outline_block.main();
document_with_toggle_list_test.main();
document_copy_and_paste_test.main();
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_test.main();
// Don't add new tests here.
}
@@ -0,0 +1,31 @@
import 'package:integration_test/integration_test.dart';
import 'document_block_option_test.dart' as document_block_option_test;
import 'document_find_menu_test.dart' as document_find_menu_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_shortcuts_test.dart' as document_shortcuts_test;
import 'document_toolbar_test.dart' as document_toolbar_test;
import 'document_with_file_test.dart' as document_with_file_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
import 'document_with_simple_table_test.dart'
as document_with_simple_table_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_with_image_block_test.main();
document_with_multi_image_block_test.main();
document_inline_page_reference_test.main();
document_more_actions_test.main();
document_with_file_test.main();
document_shortcuts_test.main();
document_block_option_test.main();
document_find_menu_test.main();
document_toolbar_test.main();
document_with_simple_table_test.main();
}
@@ -0,0 +1,373 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
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('execute undo and redo in title', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
// press a random key to make the undo stack not empty
await tester.simulateKeyEvent(LogicalKeyboardKey.keyA);
await tester.pumpAndSettle();
// undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
// wait for the undo to be applied
await tester.pumpAndSettle(Durations.long1);
// expect the title is empty
expect(
tester
.widget<TextField>(
tester.editor.findDocumentTitle(''),
)
.controller
?.text,
'',
);
// redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
isShiftPressed: true,
);
await tester.pumpAndSettle(Durations.short1);
if (UniversalPlatform.isMacOS) {
expect(
tester
.widget<TextField>(
tester.editor.findDocumentTitle(_testDocumentName),
)
.controller
?.text,
_testDocumentName,
);
}
});
testWidgets('escape key should exit the editing mode', (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.escape);
await tester.pumpAndSettle();
expect(
tester
.widget<TextField>(
tester.editor.findDocumentTitle(_testDocumentName),
)
.focusNode
?.hasFocus,
isFalse,
);
});
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),
),
);
});
testWidgets(
'hover on the cover title, check if the add icon & add cover button are shown',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.hoverOnWidget(
title,
onHover: () async {
expect(find.byType(DocumentCoverWidget), findsOneWidget);
},
);
await tester.pumpAndSettle();
});
testWidgets('paste text in title, check if the text is updated',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await Clipboard.setData(const ClipboardData(text: _testDocumentName));
final title = tester.editor.findDocumentTitle('');
await tester.tapButton(title);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isMetaPressed: UniversalPlatform.isMacOS,
isControlPressed: !UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
final newTitle = tester.editor.findDocumentTitle(_testDocumentName);
expect(newTitle, findsOneWidget);
});
});
}
@@ -0,0 +1,50 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
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() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document toolbar:', () {
testWidgets('font family', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await tester.editor.tapLineOfEditorAt(0);
const text = 'font family';
await tester.ime.insertText(text);
await tester.editor.updateSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
);
// tap the font family button
final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey);
await tester.tapButton(fontFamilyButton);
// expect to see the font family dropdown immediately
expect(find.byType(FontFamilyDropDown), findsOneWidget);
// click the font family 'Abel'
const abel = 'Abel';
await tester.tapButton(find.text(abel));
// check the text is updated to 'Abel'
final editorState = tester.editor.getCurrentEditorState();
expect(
editorState.getDeltaAttributeValueInSelection(
AppFlowyRichTextKeys.fontFamily,
),
abel,
);
});
});
}
@@ -1,5 +1,5 @@
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';
@@ -13,7 +13,7 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('cover image', () {
group('cover image:', () {
testWidgets('document cover tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -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();
@@ -0,0 +1,125 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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/util.dart';
void main() {
setUp(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
});
group('date or reminder block in document', () {
testWidgets("insert date with time block", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'Date with time test',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
);
final dateTimeSettings = DateTimeSettingsPB(
dateFormat: UserDateFormatPB.Friendly,
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final DateTime currentDateTime = DateTime.now();
final String formattedDate =
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
// get current date in editor
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
// tap on date field
await tester.tap(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// tap the toggle of include time
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
// add time 11:12
final textField = find
.descendant(
of: find.byType(DesktopAppFlowyDatePicker),
matching: find.byType(TextField),
)
.last;
await tester.pumpUntilFound(textField);
await tester.enterText(textField, "11:12");
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// we will get field with current date and 11:12 as time
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate 11:12'), findsOneWidget);
});
testWidgets("insert date with reminder block", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'Date with reminder test',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
);
final dateTimeSettings = DateTimeSettingsPB(
dateFormat: UserDateFormatPB.Friendly,
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final DateTime currentDateTime = DateTime.now();
final String formattedDate =
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
// get current date in editor
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
// tap on date field
await tester.tap(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// tap reminder and set reminder to 1 day before
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
await tester.pumpAndSettle();
await tester.tap(
find.textContaining(
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
),
);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
// we will get field with current date reminder_clock.svg icon
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
});
});
}
@@ -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');
@@ -50,9 +47,7 @@ void main() {
mockPickFilePaths(paths: [filePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tap(
find.text(LocaleKeys.document_plugins_file_fileUploadHint.tr()),
);
await tester.tapFileUploadHint();
await tester.pumpAndSettle();
expect(find.byType(FileUploadMenu), findsNothing);
@@ -116,9 +111,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
@@ -6,7 +6,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/startup/startup.dart';
@@ -25,11 +24,70 @@ import 'package:run_with_network_images/run_with_network_images.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
const _testImageUrls = [
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640',
'https://www.easygifanimator.net/images/samples/eglite.gif',
'https://people.math.sc.edu/Burkardt/data/bmp/snail.bmp',
'https://file-examples.com/storage/fe9566cb7d67345489a5a97/2017/10/file_example_JPG_100kB.jpg',
];
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
group('image block in document', () {
Future<void> testEmbedImage(WidgetTester tester, String url) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
);
await tester.enterText(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField),
),
url,
);
await tester.tapButton(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.text(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
findRichText: true,
),
),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], url);
}
testWidgets('insert an image from local file', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -80,59 +138,11 @@ void main() {
file.deleteSync();
});
testWidgets('insert an image from network', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
);
const url =
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
await tester.enterText(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField),
),
url,
);
await tester.tapButton(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.text(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
findRichText: true,
),
),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], url);
});
for (final url in _testImageUrls) {
testWidgets('insert an image from network: $url', (tester) async {
await testEmbedImage(tester, url);
});
}
testWidgets('insert an image from unsplash', (tester) async {
await runWithNetworkImages(() async {
@@ -160,11 +170,7 @@ void main() {
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
'Unsplash',
);
expect(find.byType(UnsplashImageWidget), findsOneWidget);
expect(find.text('Unsplash'), findsOneWidget);
});
});
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -92,7 +92,7 @@ void main() {
);
expect(finder, findsOneWidget);
await tester.tapButton(finder);
expect(find.byType(AppFlowyErrorPage), findsOneWidget);
expect(find.byType(GridPage), findsOneWidget);
});
});
}
@@ -6,7 +6,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
@@ -26,7 +25,6 @@ import 'package:path_provider/path_provider.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
setUp(() {
@@ -50,7 +48,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
offset: 80,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
@@ -148,7 +146,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
offset: 80,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
@@ -1,9 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.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';
@@ -46,6 +44,9 @@ void main() {
* # Heading 1
* ## Heading 2
* ### Heading 3
* > # Heading 1
* > ## Heading 2
* > ### Heading 3
*/
await tester.editor.tapLineOfEditorAt(3);
@@ -56,7 +57,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading1),
),
findsOneWidget,
findsNWidgets(2),
);
// Heading 2 is prefixed with a bullet
@@ -65,7 +66,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading2),
),
findsOneWidget,
findsNWidgets(2),
);
// Heading 3 is prefixed with a dash
@@ -74,7 +75,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading3),
),
findsOneWidget,
findsNWidgets(2),
);
// update the Heading 1 to Heading 1Hello world
@@ -102,13 +103,16 @@ void main() {
* # Heading 1
* ## Heading 2
* ### Heading 3
* > # Heading 1
* > ## Heading 2
* > ### Heading 3
*/
await tester.editor.tapLineOfEditorAt(3);
await tester.editor.tapLineOfEditorAt(7);
await insertOutlineInDocument(tester);
// expect to find only the `heading1` widget under the [OutlineBlockWidget]
await hoverAndClickDepthOptionAction(tester, [3], 1);
await hoverAndClickDepthOptionAction(tester, [6], 1);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
@@ -126,7 +130,7 @@ void main() {
//////
/// expect to find only the 'heading1' and 'heading2' under the [OutlineBlockWidget]
await hoverAndClickDepthOptionAction(tester, [3], 2);
await hoverAndClickDepthOptionAction(tester, [6], 2);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
@@ -137,13 +141,13 @@ void main() {
//////
// expect to find all the headings under the [OutlineBlockWidget]
await hoverAndClickDepthOptionAction(tester, [3], 3);
await hoverAndClickDepthOptionAction(tester, [6], 3);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text(heading1),
),
findsOneWidget,
findsNWidgets(2),
);
expect(
@@ -151,7 +155,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading2),
),
findsOneWidget,
findsNWidgets(2),
);
expect(
@@ -159,7 +163,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading3),
),
findsOneWidget,
findsNWidgets(2),
);
//////
});
@@ -172,7 +176,6 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_outline.tr(),
offset: 100,
);
await tester.pumpAndSettle();
}
@@ -182,19 +185,25 @@ Future<void> hoverAndClickDepthOptionAction(
List<int> path,
int level,
) async {
await tester.editor.hoverAndClickOptionMenuButton([3]);
await tester.tap(find.byType(AppFlowyPopover).hitTestable().last);
await tester.pumpAndSettle();
// Find a total of 4 HoverButtons under the [BlockOptionButton],
// in addition to 3 HoverButtons under the [DepthOptionAction] - (child of BlockOptionButton)
await tester.tap(find.byType(HoverButton).hitTestable().at(3 + level));
await tester.editor.openDepthMenu(path);
final type = OptionDepthType.fromLevel(level);
await tester.tapButton(find.findTextInFlowyText(type.description));
await tester.pumpAndSettle();
}
Future<void> insertHeadingComponent(WidgetTester tester) async {
await tester.editor.tapLineOfEditorAt(0);
// # heading 1-3
await tester.ime.insertText('# $heading1\n');
await tester.ime.insertText('## $heading2\n');
await tester.ime.insertText('### $heading3\n');
// > # toggle heading 1-3
await tester.ime.insertText('> # $heading1\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
await tester.ime.insertText('> ## $heading2\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
await tester.ime.insertText('> ### $heading3\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
}
@@ -0,0 +1,370 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy_editor/appflowy_editor.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:universal_platform/universal_platform.dart';
import '../../shared/util.dart';
const String heading1 = "Heading 1";
const String heading2 = "Heading 2";
const String heading3 = "Heading 3";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('simple table block test:', () {
testWidgets('insert a simple table block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// validate the table is inserted
expect(find.byType(SimpleTableBlockWidget), findsOneWidget);
final editorState = tester.editor.getCurrentEditorState();
expect(
editorState.selection,
// table -> row -> cell -> paragraph
Selection.collapsed(Position(path: [0, 0, 0, 0])),
);
final firstCell = find.byType(SimpleTableCellBlockWidget).first;
expect(
tester
.state<SimpleTableCellBlockWidgetState>(firstCell)
.isEditingCellNotifier
.value,
isTrue,
);
});
testWidgets('select all in table cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
const cell1Content = 'Cell 1';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('New Table');
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
await tester.editor.tapLineOfEditorAt(1);
await tester.insertTableInDocument();
await tester.ime.insertText(cell1Content);
await tester.pumpAndSettle();
// Select all in the cell
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection(
start: Position(path: [1, 0, 0, 0]),
end: Position(path: [1, 0, 0, 0], offset: cell1Content.length),
),
);
// Press select all again, the selection should be the entire document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection(
start: Position(path: [0]),
end: Position(path: [1, 1, 1, 0]),
),
);
});
testWidgets('''
1. hover on the table
1.1 click the add row button
1.2 click the add column button
1.3 click the add row and column button
2. validate the table is updated
3. delete the last column
4. delete the last row
5. validate the table is updated
''', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// hover on the table
final tableBlock = find.byType(SimpleTableBlockWidget).first;
await tester.hoverOnWidget(
tableBlock,
onHover: () async {
// click the add row button
final addRowButton = find.byType(SimpleTableAddRowButton).first;
await tester.tap(addRowButton);
// click the add column button
final addColumnButton = find.byType(SimpleTableAddColumnButton).first;
await tester.tap(addColumnButton);
// click the add row and column button
final addRowAndColumnButton =
find.byType(SimpleTableAddColumnAndRowButton).first;
await tester.tap(addRowAndColumnButton);
},
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 4);
expect(tableNode.rowLength, 4);
// delete the last row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: tableNode.rowLength - 1,
action: SimpleTableMoreAction.delete,
);
await tester.pumpAndSettle();
expect(tableNode.rowLength, 3);
expect(tableNode.columnLength, 4);
// delete the last column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: tableNode.columnLength - 1,
action: SimpleTableMoreAction.delete,
);
await tester.pumpAndSettle();
expect(tableNode.columnLength, 3);
expect(tableNode.rowLength, 3);
});
testWidgets('enable header column and header row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// enable the header row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.enableHeaderRow,
);
await tester.pumpAndSettle();
// enable the header column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.enableHeaderColumn,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.isHeaderColumnEnabled, isTrue);
expect(tableNode.isHeaderRowEnabled, isTrue);
// disable the header row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.enableHeaderRow,
);
await tester.pumpAndSettle();
expect(tableNode.isHeaderColumnEnabled, isTrue);
expect(tableNode.isHeaderRowEnabled, isFalse);
// disable the header column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.enableHeaderColumn,
);
await tester.pumpAndSettle();
expect(tableNode.isHeaderColumnEnabled, isFalse);
expect(tableNode.isHeaderRowEnabled, isFalse);
});
testWidgets('duplicate a column / row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// duplicate the row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.duplicate,
);
await tester.pumpAndSettle();
// duplicate the column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.duplicate,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 3);
expect(tableNode.rowLength, 3);
});
testWidgets('insert left / insert right', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// insert left
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.insertLeft,
);
await tester.pumpAndSettle();
// insert right
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.insertRight,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 4);
expect(tableNode.rowLength, 2);
});
testWidgets('insert above / insert below', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// insert above
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.insertAbove,
);
await tester.pumpAndSettle();
// insert below
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.insertBelow,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.rowLength, 4);
expect(tableNode.columnLength, 2);
});
});
}
extension on WidgetTester {
/// Insert a table in the document
Future<void> insertTableInDocument() async {
// open the actions menu and insert the outline block
await editor.showSlashMenu();
await editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_table.tr(),
);
await pumpAndSettle();
}
Future<void> clickMoreActionItemInTableMenu({
required SimpleTableMoreActionType type,
required int index,
required SimpleTableMoreAction action,
}) async {
if (type == SimpleTableMoreActionType.row) {
final row = find.byWidgetPredicate((w) {
return w is SimpleTableRowBlockWidget && w.node.rowIndex == index;
});
await hoverOnWidget(
row,
onHover: () async {
final moreActionButton = find.byWidgetPredicate((w) {
return w is SimpleTableMoreActionMenu &&
w.type == SimpleTableMoreActionType.row &&
w.index == index;
});
await tapButton(moreActionButton);
await tapButton(find.text(action.name));
},
);
await pumpAndSettle();
} else if (type == SimpleTableMoreActionType.column) {
final column = find.byWidgetPredicate((w) {
return w is SimpleTableCellBlockWidget && w.node.columnIndex == index;
}).first;
await hoverOnWidget(
column,
onHover: () async {
final moreActionButton = find.byWidgetPredicate((w) {
return w is SimpleTableMoreActionMenu &&
w.type == SimpleTableMoreActionType.column &&
w.index == index;
});
await tapButton(moreActionButton);
await tapButton(find.text(action.name));
},
);
await pumpAndSettle();
}
await tapAt(Offset.zero);
}
}
@@ -0,0 +1,128 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.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 '../../shared/util.dart';
const String _heading1 = 'Heading 1';
const String _heading2 = 'Heading 2';
const String _heading3 = 'Heading 3';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('toggle heading block test:', () {
testWidgets('insert toggle heading 1 - 3 block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'toggle heading block test',
);
for (var i = 1; i <= 3; i++) {
await tester.editor.tapLineOfEditorAt(0);
await _insertToggleHeadingBlockInDocument(tester, i);
await tester.pumpAndSettle();
expect(
find.byWidgetPredicate(
(widget) =>
widget is ToggleListBlockComponentWidget &&
widget.node.attributes[ToggleListBlockKeys.level] == i,
),
findsOneWidget,
);
}
});
testWidgets('insert toggle heading 1 - 3 block by shortcuts',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'toggle heading block test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# > $_heading1\n');
await tester.ime.insertText('## > $_heading2\n');
await tester.ime.insertText('### > $_heading3\n');
await tester.ime.insertText('> # $_heading1\n');
await tester.ime.insertText('> ## $_heading2\n');
await tester.ime.insertText('> ### $_heading3\n');
await tester.pumpAndSettle();
expect(
find.byType(ToggleListBlockComponentWidget),
findsNWidgets(6),
);
});
testWidgets('insert toggle heading and convert it to heading',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'toggle heading block test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# > $_heading1\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.ime.insertText('item 1');
await tester.pumpAndSettle();
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: _heading1.length),
),
);
await tester.tapButton(find.byType(HeadingPopup));
await tester.pumpAndSettle();
expect(
find.byType(HeadingButton),
findsNWidgets(3),
);
// tap the H1 button
await tester.tapButton(find.byType(HeadingButton).at(0));
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final node1 = editorState.document.nodeAtPath([0])!;
expect(node1.type, HeadingBlockKeys.type);
expect(node1.attributes[HeadingBlockKeys.level], 1);
final node2 = editorState.document.nodeAtPath([1])!;
expect(node2.type, ParagraphBlockKeys.type);
expect(node2.delta!.toPlainText(), 'item 1');
});
});
}
Future<void> _insertToggleHeadingBlockInDocument(
WidgetTester tester,
int level,
) async {
final name = switch (level) {
1 => LocaleKeys.document_slashMenu_name_toggleHeading1.tr(),
2 => LocaleKeys.document_slashMenu_name_toggleHeading2.tr(),
3 => LocaleKeys.document_slashMenu_name_toggleHeading3.tr(),
_ => throw Exception('Invalid level: $level'),
};
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
name,
offset: 150,
);
await tester.pumpAndSettle();
}
@@ -1,7 +1,9 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
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_test/flutter_test.dart';
@@ -214,5 +216,73 @@ void main() {
expectToggleListOpened();
});
Future<void> prepareToggleHeadingBlock(
WidgetTester tester,
String text,
) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(text);
}
testWidgets('> + # to toggle heading 1 block', (tester) async {
await prepareToggleHeadingBlock(tester, '> # Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 1);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('> + ### to toggle heading 3 block', (tester) async {
await prepareToggleHeadingBlock(tester, '> ### Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 3);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('# + > to toggle heading 1 block', (tester) async {
await prepareToggleHeadingBlock(tester, '# > Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 1);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('### + > to toggle heading 3 block', (tester) async {
await prepareToggleHeadingBlock(tester, '### > Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 3);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('click the toggle list to create a new paragraph',
(tester) async {
await prepareToggleHeadingBlock(tester, '> # Hello');
final emptyHintText = find.text(
LocaleKeys.document_plugins_emptyToggleHeading.tr(
args: ['1'],
),
);
expect(emptyHintText, findsOneWidget);
await tester.tapButton(emptyHintText);
await tester.pumpAndSettle();
// check the new paragraph is created
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0, 0])!;
expect(node.type, ParagraphBlockKeys.type);
});
});
}
@@ -11,7 +11,7 @@ void main() {
testWidgets('empty test', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.wait(1000);
await tester.wait(500);
});
});
}
@@ -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,234 @@
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 click the delete button
await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu();
});
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
tester.assertNumberOfRowsInGridPage(2);
});
testWidgets('delete row in two views', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.renameLinkedView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid),
'grid 1',
);
tester.assertNumberOfRowsInGridPage(3);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
await tester.renameLinkedView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid).at(1),
'grid 2',
);
tester.assertNumberOfRowsInGridPage(3);
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and click the delete button
await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu();
});
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
// 3 initial rows - 1 deleted
tester.assertNumberOfRowsInGridPage(2);
await tester.tapTabBarLinkedViewByViewName('grid 1');
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),
];
}
}
@@ -0,0 +1,19 @@
import 'package:integration_test/integration_test.dart';
import 'grid_edit_row_test.dart' as grid_edit_row_test_runner;
import 'grid_filter_and_sort_test.dart' as grid_filter_and_sort_test_runner;
import 'grid_reopen_test.dart' as grid_reopen_test_runner;
import 'grid_reorder_row_test.dart' as grid_reorder_row_test_runner;
import 'grid_row_test.dart' as grid_row_test_runner;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
grid_reopen_test_runner.main();
grid_row_test_runner.main();
grid_reorder_row_test_runner.main();
grid_filter_and_sort_test_runner.main();
grid_edit_row_test_runner.main();
// grid_calculations_test_runner.main();
// DON'T add more tests here.
}
@@ -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();
}
Loaded 100 of 1820 files, more files were not shown because too many files have changed in this diff. Show more