Compare commits

...
104 Commits
Author SHA1 Message Date
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
829 changed files with 26957 additions and 13586 deletions

No files matched your search

+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
+21 -78
View File
@@ -28,7 +28,7 @@ env:
FLUTTER_VERSION: "3.22.2"
RUST_TOOLCHAIN: "1.80.1"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.34-amd64
CLOUD_VERSION: 0.6.54-amd64
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -262,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
@@ -324,14 +339,14 @@ jobs:
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:
integration_test:
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
@@ -340,82 +355,10 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- 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
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: 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: 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 }}
integration_test_4:
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: Flutter Integration Test 4
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_4.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 }}
+22 -18
View File
@@ -20,9 +20,8 @@ on:
- "!frontend/appflowy_web_app/**"
env:
FLUTTER_VERSION: "3.22.0"
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.80.1"
CLOUD_VERSION: 0.6.34-amd64
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -97,21 +96,26 @@ jobs:
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
cargo make --profile development-ios-arm64-sim code_generation
# - uses: futureware-tech/simulator-action@v3
# id: simulator-action
# with:
# model: "iPhone 15"
# shutdown_after_job: false
- uses: futureware-tech/simulator-action@v3
id: simulator-action
with:
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
# - 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 }}
+23 -21
View File
@@ -15,12 +15,10 @@ on:
- "main"
- "develop"
- "release/*"
paths:
- "frontend/rust-lib/**"
env:
CARGO_TERM_COLOR: always
CLOUD_VERSION: 0.6.34-amd64
CLOUD_VERSION: 0.7.6-amd64
RUST_TOOLCHAIN: "1.77.2"
jobs:
@@ -43,6 +41,7 @@ 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
@@ -66,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
@@ -135,7 +136,7 @@ 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|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
@@ -145,26 +146,27 @@ jobs:
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:
+26
View File
@@ -1,4 +1,30 @@
# Release Notes
# Release Notes
## 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
+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.7.2"
APPFLOWY_VERSION = "0.7.4"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database/board/presentation/widgets/board_colum
import 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -121,22 +120,3 @@ void main() {
});
});
}
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();
@@ -1,33 +1,10 @@
// 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 '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -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,
);
});
});
}
@@ -3,10 +3,12 @@ import 'package:integration_test/integration_test.dart';
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();
}
@@ -1,35 +1,13 @@
// 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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:universal_platform/universal_platform.dart';
import '../../../shared/constants.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
@@ -1,21 +1,12 @@
// 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';
void main() {
@@ -1,24 +1,9 @@
// 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';
void 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 '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -1,21 +1,12 @@
// 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';
@@ -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),
);
@@ -1,38 +1,19 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('collaborative workspace: ', () {
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 {
@@ -93,5 +74,79 @@ void main() {
},
);
});
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,
);
},
);
});
});
}
@@ -1,28 +1,17 @@
// ignore_for_file: unused_import
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/plugins/shared/share/share_menu.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -61,11 +50,11 @@ void main() {
final plainText = clipboardContent.plainText;
expect(
plainText,
startsWith(ShareConstants.shareBaseUrl),
matches(appflowySharePageLinkPattern),
);
final shareValues = plainText!
.replaceAll('${ShareConstants.shareBaseUrl}/', '')
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, 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);
});
});
}
@@ -1,35 +1,11 @@
// 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_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:universal_platform/universal_platform.dart';
import '../../../shared/constants.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
@@ -1,36 +1,23 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/plugins/shared/share/publish_tab.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/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/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
@@ -93,4 +80,266 @@ void main() {
},
);
});
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);
}
});
});
}
@@ -3,6 +3,7 @@ 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;
@@ -14,4 +15,5 @@ void main() {
collaborative_workspace_test.main();
change_name_and_icon_test.main();
workspace_icon_test.main();
tabs_test.main();
}
@@ -211,7 +211,7 @@ void main() {
await tester.toggleIncludeTime();
// Select a date
final now = DateTime.now();
DateTime now = DateTime.now();
await tester.selectDay(content: now.day);
await tester.dismissCellEditor();
@@ -225,7 +225,7 @@ void main() {
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
// Toggle include time
// When toggling include time, the time value is from the previous existing date time, not the current time
now = DateTime.now();
await tester.toggleIncludeTime();
await tester.dismissCellEditor();
@@ -582,7 +582,6 @@ void main() {
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y').format(now),
// content: DateFormat('MMM dd, y HH:mm').format(now),
);
tester.assertCellContent(
rowIndex: 1,
@@ -21,7 +21,6 @@ import 'package:path_provider/path_provider.dart';
import '../../shared/database_test_op.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -69,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);
@@ -85,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
@@ -139,10 +133,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 two files
expect(find.byType(RenderMedia), findsNWidgets(2));
@@ -193,10 +184,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 two files
expect(find.byType(RenderMedia), findsNWidgets(2));
@@ -230,7 +218,7 @@ void main() {
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('hide file names', (tester) async {
testWidgets('show file names', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -272,10 +260,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 two files
expect(find.byType(RenderMedia), findsNWidgets(2));
@@ -283,28 +268,28 @@ void main() {
await tester.dismissCellEditor();
await tester.pumpAndSettle();
// Open first row in row detail view then toggle hide file names
// 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.tapGridFieldWithNameInRowDetailPage('Type');
await tester.pumpAndSettle();
// Toggle hide file names
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
await tester.dismissRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to be hidden
expect(find.text('sample.jpeg'), findsNothing);
expect(find.text('sample.gif'), findsNothing);
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
@@ -62,9 +62,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.tapFileUploadHint();
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
@@ -1,17 +1,17 @@
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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/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:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/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';
@@ -76,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 {
@@ -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,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);
});
});
}
@@ -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,
);
},
);
}
@@ -1,6 +1,5 @@
import 'dart:io';
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_page_block.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
@@ -14,7 +13,6 @@ import 'package:integration_test/integration_test.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
const _firstDocName = "Inline Sub Page Mention";
const _createdPageName = "hi world";
@@ -52,7 +50,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Delete from editor
await tester.editor.updateSelection(
@@ -126,7 +123,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Cut from editor
await tester.editor.updateSelection(
@@ -152,7 +148,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Cut again
await tester.editor.updateSelection(
@@ -196,7 +191,6 @@ void main() {
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
});
testWidgets(
'Cut+paste in same docuemnt and then paste again in same document',
@@ -215,7 +209,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Cut from editor
await tester.editor.updateSelection(
@@ -241,7 +234,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Paste again
await tester.simulateKeyEvent(
@@ -253,7 +245,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsNWidgets(2));
expect(find.text('$_createdPageName (copy)'), findsNWidgets(2));
});
@@ -272,7 +263,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
final headingText = LocaleKeys.document_slashMenu_name_heading1.tr();
final paragraphText = LocaleKeys.document_slashMenu_name_text.tr();
@@ -284,7 +274,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Turn into paragraph
await tester.editor.openTurnIntoMenu([0]);
@@ -293,7 +282,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
});
testWidgets('Duplicate a block containing two sub page mentions',
@@ -332,7 +320,6 @@ void main() {
expect(find.text(_createdPageName), findsOneWidget);
expect(find.text("$_createdPageName (copy)"), findsOneWidget);
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsNWidgets(2));
// Duplicate node from block action menu
await tester.editor.hoverAndClickOptionMenuButton([0]);
@@ -1,13 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
import 'package:appflowy/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';
@@ -73,10 +73,7 @@ void main() {
);
await tester.pumpAndSettle();
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@@ -99,10 +96,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
@@ -155,10 +149,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
@@ -216,10 +207,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
@@ -260,10 +248,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
@@ -313,10 +298,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@@ -326,6 +308,11 @@ void main() {
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,
@@ -354,10 +341,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
// Delete
@@ -405,15 +389,16 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
await tester.hoverOnPageName('Child page');
await tester.tapDeletePageButton();
await tester.hoverOnPageName(
'Child page',
onHover: () async {
await tester.tapDeletePageButton();
},
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Child page'), findsNothing);
@@ -432,10 +417,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@@ -473,6 +455,49 @@ void main() {
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));
});
});
}
@@ -498,4 +523,16 @@ extension _SubPageTestHelper on WidgetTester {
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,26 +1,12 @@
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_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_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 main() {
@@ -33,17 +19,5 @@ void 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_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();
// Don't add new tests here. Add them to document_test_runner_2.dart
// Don't add new tests here.
}
@@ -2,22 +2,25 @@ 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_inline_sub_page_test.dart' as document_inline_sub_page_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();
// Disable subPage test temporarily, enable it in version 0.7.2
// document_sub_page_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,28 @@
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;
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();
}
@@ -347,5 +347,27 @@ void main() {
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,
);
});
});
}
@@ -2,6 +2,7 @@ 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';
@@ -10,7 +11,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
setUp(() {
@@ -33,7 +33,6 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
offset: 100,
);
final dateTimeSettings = DateTimeSettingsPB(
@@ -57,13 +56,15 @@ void main() {
await tester.pumpAndSettle();
// add time 11:12
final currentTime = DateFormat('HH:mm').format(DateTime.now());
final textField = find.byWidgetPredicate(
(widget) =>
widget is TextField && widget.controller!.text == currentTime,
);
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.editor.tapLineOfEditorAt(0);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// we will get field with current date and 11:12 as time
@@ -85,7 +86,6 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
offset: 100,
);
final dateTimeSettings = DateTimeSettingsPB(
@@ -47,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);
@@ -24,11 +24,71 @@ 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://mathiasbynens.be/demo/animated-webp-supported.webp',
'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();
@@ -79,59 +139,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 {
@@ -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(() {
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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';
@@ -43,6 +44,9 @@ void main() {
* # Heading 1
* ## Heading 2
* ### Heading 3
* > # Heading 1
* > ## Heading 2
* > ### Heading 3
*/
await tester.editor.tapLineOfEditorAt(3);
@@ -53,7 +57,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading1),
),
findsOneWidget,
findsNWidgets(2),
);
// Heading 2 is prefixed with a bullet
@@ -62,7 +66,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading2),
),
findsOneWidget,
findsNWidgets(2),
);
// Heading 3 is prefixed with a dash
@@ -71,7 +75,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading3),
),
findsOneWidget,
findsNWidgets(2),
);
// update the Heading 1 to Heading 1Hello world
@@ -99,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),
@@ -123,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),
@@ -134,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(
@@ -148,7 +155,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading2),
),
findsOneWidget,
findsNWidgets(2),
);
expect(
@@ -156,7 +163,7 @@ void main() {
of: find.byType(OutlineBlockWidget),
matching: find.text(heading3),
),
findsOneWidget,
findsNWidgets(2),
);
//////
});
@@ -169,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();
}
@@ -187,7 +193,17 @@ Future<void> hoverAndClickDepthOptionAction(
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,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();
}
@@ -214,5 +214,54 @@ 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');
});
});
}
@@ -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,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.
}
@@ -10,7 +10,6 @@ import 'package:integration_test/integration_test.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -19,8 +19,13 @@ void main() {
await tester.tapAnonymousSignInButton();
// Right click on the view item and change icon
await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);
await tester.pumpAndSettle();
await tester.hoverOnWidget(
find.byType(ViewItem),
onHover: () async {
await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);
await tester.pumpAndSettle();
},
);
// Change icon
final changeIconButton =
@@ -1,17 +1,18 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.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:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.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/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/expectation.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
const _documentName = 'First Doc';
const _documentTwoName = 'Second Doc';
@@ -20,17 +21,12 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tabs', () {
testWidgets('Open AppFlowy and open/navigate/close tabs', (tester) async {
testWidgets('open/navigate/close tabs', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsNothing,
);
// No tabs rendered yet
expect(find.byType(FlowyTab), findsNothing);
await tester.createNewPageWithNameUnderParent(name: _documentName);
@@ -44,7 +40,7 @@ void main() {
expect(
find.descendant(
of: find.byType(TabBar),
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(3),
@@ -71,11 +67,83 @@ void main() {
expect(
find.descendant(
of: find.byType(TabBar),
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(2),
);
});
testWidgets('right click show tab menu, close others', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsNothing,
);
await tester.createNewPageWithNameUnderParent(name: _documentName);
await tester.createNewPageWithNameUnderParent(name: _documentTwoName);
/// Open second menu item in a new tab
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
/// Open third menu item in a new tab
await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(3),
);
/// Right click on second tab
await tester.tap(
buttons: kSecondaryButton,
find.descendant(
of: find.byType(FlowyTab),
matching: find.text(gettingStarted),
),
);
await tester.pumpAndSettle();
expect(find.byType(TabMenu), findsOneWidget);
final firstTabFinder = find.descendant(
of: find.byType(FlowyTab),
matching: find.text(_documentTwoName),
);
final secondTabFinder = find.descendant(
of: find.byType(FlowyTab),
matching: find.text(gettingStarted),
);
final thirdTabFinder = find.descendant(
of: find.byType(FlowyTab),
matching: find.text(_documentName),
);
expect(firstTabFinder, findsOneWidget);
expect(secondTabFinder, findsOneWidget);
expect(thirdTabFinder, findsOneWidget);
// Close other tabs than the second item
await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr()));
await tester.pumpAndSettle();
// We expect to not find any tabs
expect(firstTabFinder, findsNothing);
expect(secondTabFinder, findsNothing);
expect(thirdTabFinder, findsNothing);
// Expect second tab to be current page (current page has breadcrumb, cover title,
// and in this case view name in sidebar)
expect(find.text(gettingStarted), findsNWidgets(3));
});
});
}
@@ -0,0 +1,21 @@
import 'package:integration_test/integration_test.dart';
import 'emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'hotkeys_test.dart' as hotkeys_test;
import 'import_files_test.dart' as import_files_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'zoom_in_out_test.dart' as zoom_in_out_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
hotkeys_test.main();
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
share_markdown_test.main();
import_files_test.main();
zoom_in_out_test.main();
// DON'T add more tests here.
}
@@ -1,7 +1,6 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_1.dart' as document_test_runner;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/document/document_test_runner_1.dart' as document_test_runner_1;
import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test;
Future<void> main() async {
@@ -11,11 +10,7 @@ Future<void> main() async {
Future<void> runIntegration1OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
switch_folder_test.main();
document_test_runner.main();
document_test_runner_1.main();
// DON'T add more tests here.
}
@@ -1,18 +1,7 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/database/database_calendar_test.dart' as database_calendar_test;
import 'desktop/database/database_cell_test.dart' as database_cell_test;
import 'desktop/database/database_field_settings_test.dart'
as database_field_settings_test;
import 'desktop/database/database_field_test.dart' as database_field_test;
import 'desktop/database/database_filter_test.dart' as database_filter_test;
import 'desktop/database/database_media_test.dart' as database_media_test;
import 'desktop/database/database_row_page_test.dart' as database_row_page_test;
import 'desktop/database/database_setting_test.dart' as database_setting_test;
import 'desktop/database/database_share_test.dart' as database_share_test;
import 'desktop/database/database_sort_test.dart' as database_sort_test;
import 'desktop/database/database_view_test.dart' as database_view_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/database/database_test_runner_1.dart' as database_test_runner_1;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration2OnDesktop();
@@ -21,20 +10,8 @@ Future<void> main() async {
Future<void> runIntegration2OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
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();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
database_media_test.main();
database_test_runner_1.main();
// DON'T add more tests here. This is the second test runner for desktop.
}
@@ -1,24 +1,8 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/database/database_row_cover_test.dart'
as database_row_cover_test;
import 'desktop/grid/grid_edit_row_test.dart' as grid_edit_row_test_runner;
import 'desktop/grid/grid_filter_and_sort_test.dart'
as grid_filter_and_sort_test_runner;
import 'desktop/grid/grid_reopen_test.dart' as grid_reopen_test_runner;
import 'desktop/grid/grid_reorder_row_test.dart'
as grid_reorder_row_test_runner;
import 'desktop/grid/grid_row_test.dart' as grid_create_row_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
import 'desktop/uncategorized/import_files_test.dart' as import_files_test;
import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/uncategorized/zoom_in_out_test.dart' as zoom_in_out_test;
import 'desktop/first_test/first_test.dart' as first_test;
import 'desktop/grid/grid_test_runner_1.dart' as grid_test_runner_1;
Future<void> main() async {
await runIntegration3OnDesktop();
@@ -27,26 +11,9 @@ Future<void> main() async {
Future<void> runIntegration3OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
settings_test_runner.main();
share_markdown_test.main();
import_files_test.main();
sidebar_test_runner.main();
board_test_runner.main();
tabs_test.main();
database_row_cover_test.main();
grid_reopen_test_runner.main();
grid_create_row_test_runner.main();
grid_reorder_row_test_runner.main();
grid_filter_and_sort_test_runner.main();
grid_edit_row_test_runner.main();
zoom_in_out_test.main();
// DON'T add more tests here. Add them to desktop_runner_4.dart
grid_test_runner_1.main();
// DON'T add more tests here.
}
@@ -1,7 +1,7 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_2.dart' as document_test_runner_2;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration4OnDesktop();
@@ -10,8 +10,8 @@ Future<void> main() async {
Future<void> runIntegration4OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
document_test_runner_2.main();
// DON'T add more tests here.
}
@@ -0,0 +1,17 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/database/database_test_runner_2.dart' as database_test_runner_2;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration5OnDesktop();
}
Future<void> runIntegration5OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
database_test_runner_2.main();
// DON'T add more tests here.
}
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/first_test/first_test.dart' as first_test;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/uncategorized_test_runner_1.dart'
as uncategorized_test_runner_1;
Future<void> main() async {
await runIntegration6OnDesktop();
}
Future<void> runIntegration6OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
settings_test_runner.main();
sidebar_test_runner.main();
uncategorized_test_runner_1.main();
// DON'T add more tests here.
}
@@ -0,0 +1,17 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_3.dart' as document_test_runner_3;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration7OnDesktop();
}
Future<void> runIntegration7OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
document_test_runner_3.main();
// DON'T add more tests here.
}
@@ -0,0 +1,17 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_4.dart' as document_test_runner_4;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration8OnDesktop();
}
Future<void> runIntegration8OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
document_test_runner_4.main();
// DON'T add more tests here.
}
@@ -0,0 +1,16 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration9OnDesktop();
}
Future<void> runIntegration9OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
tabs_test.main();
}
@@ -1,9 +1,11 @@
import 'document/publish_test.dart' as publish_test;
import 'document/share_link_test.dart' as share_link_test;
import 'space/space_test.dart' as space_test;
import 'workspace/workspace_operations_test.dart' as workspace_operations_test;
Future<void> main() async {
workspace_operations_test.main();
share_link_test.main();
publish_test.main();
space_test.main();
}
@@ -1,47 +1,23 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'dart:math';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.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:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('publish:', () {
testWidgets('publish document', (tester) async {
testWidgets('''
1. publish document
2. update path name
3. unpublish document
''', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
@@ -75,6 +51,51 @@ void main() {
findsOneWidget,
);
// update the path name
await tester.editor.clickMoreActionItemOnMobile(
LocaleKeys.shareAction_updatePathName.tr(),
);
const pathName1 = '???????????????';
const pathName2 = 'AppFlowy';
final textField = find.descendant(
of: find.byType(EditWorkspaceNameBottomSheet),
matching: find.byType(TextFormField),
);
await tester.enterText(textField, pathName1);
await tester.pumpAndSettle();
// wait 50ms to ensure the error message is shown
await tester.wait(50);
// click the confirm button
final confirmButton = find.text(LocaleKeys.button_confirm.tr());
await tester.tapButton(confirmButton);
// expect to see the update path name failed toast
final updatePathFailedText = find.text(
LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters
.tr(),
);
expect(updatePathFailedText, findsOneWidget);
// input the valid path name
await tester.enterText(textField, pathName2);
await tester.pumpAndSettle();
// click the confirm button
await tester.tapButton(confirmButton);
// wait 50ms to ensure the error message is shown
await tester.wait(50);
// expect to see the update path name success toast
final updatePathSuccessText = find.findTextInFlowyText(
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
expect(updatePathSuccessText, findsOneWidget);
await tester.pumpUntilNotFound(updatePathSuccessText);
// unpublish the document
await tester.editor.clickMoreActionItemOnMobile(
LocaleKeys.shareAction_unPublish.tr(),
@@ -1,39 +1,12 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
@@ -0,0 +1,287 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/space/manage_space_widget.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';
import 'package:appflowy/mobile/presentation/home/space/space_menu_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/space/widgets.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.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('space operations:', () {
Future<void> openSpaceMenu(WidgetTester tester) async {
final spaceHeader = find.byType(MobileSpaceHeader);
await tester.tapButton(spaceHeader);
await tester.pumpUntilFound(find.byType(MobileSpaceMenu));
}
Future<void> openSpaceMenuMoreOptions(
WidgetTester tester,
ViewPB space,
) async {
final spaceMenuItemTrailing = find.byWidgetPredicate(
(w) => w is SpaceMenuItemTrailing && w.space.id == space.id,
);
final moreOptions = find.descendant(
of: spaceMenuItemTrailing,
matching: find.byWidgetPredicate(
(w) =>
w is FlowySvg &&
w.svg.path == FlowySvgs.workspace_three_dots_s.path,
),
);
await tester.tapButton(moreOptions);
await tester.pumpUntilFound(find.byType(SpaceMenuMoreOptions));
}
// combine the tests together to reduce the CI time
testWidgets('''
1. create a new space
2. update the space name
3. update the space permission
4. update the space icon
5. delete the space
''', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// 1. create a new space
// click the space menu
await openSpaceMenu(tester);
// click the create a new space button
final createNewSpaceButton = find.text(
LocaleKeys.space_createNewSpace.tr(),
);
await tester.pumpUntilFound(createNewSpaceButton);
await tester.tapButton(createNewSpaceButton);
// input the new space name
final inputField = find.descendant(
of: find.byType(ManageSpaceWidget),
matching: find.byType(TextField),
);
const newSpaceName = 'AppFlowy';
await tester.enterText(inputField, newSpaceName);
await tester.pumpAndSettle();
// change the space permission to private
final permissionOption = find.byType(ManageSpacePermissionOption);
await tester.tapButton(permissionOption);
await tester.pumpAndSettle();
final privateOption = find.text(LocaleKeys.space_privatePermission.tr());
await tester.tapButton(privateOption);
await tester.pumpAndSettle();
// change the space icon color
final color = builtInSpaceColors[1];
final iconOption = find.descendant(
of: find.byType(ManageSpaceIconOption),
matching: find.byWidgetPredicate(
(w) => w is SpaceColorItem && w.color == color,
),
);
await tester.tapButton(iconOption);
await tester.pumpAndSettle();
// change the space icon
final icon = kIconGroups![0].icons[1];
final iconItem = find.descendant(
of: find.byType(ManageSpaceIconOption),
matching: find.byWidgetPredicate(
(w) => w is SpaceIconItem && w.icon == icon,
),
);
await tester.tapButton(iconItem);
await tester.pumpAndSettle();
// click the done button
final doneButton = find.descendant(
of: find.byWidgetPredicate(
(w) =>
w is BottomSheetHeader &&
w.title == LocaleKeys.space_createSpace.tr(),
),
matching: find.text(LocaleKeys.button_done.tr()),
);
await tester.tapButton(doneButton);
await tester.pumpAndSettle();
// wait 100ms for the space to be created
await tester.wait(100);
// verify the space is created
await openSpaceMenu(tester);
final spaceItems = find.byType(MobileSpaceMenuItem);
// expect to see 3 space items, 2 are built-in, 1 is the new space
expect(spaceItems, findsNWidgets(3));
// convert the space item to a widget
final spaceWidget =
tester.widgetList<MobileSpaceMenuItem>(spaceItems).last;
final space = spaceWidget.space;
expect(space.name, newSpaceName);
expect(space.spacePermission, SpacePermission.private);
expect(space.spaceIcon, icon.iconPath);
expect(space.spaceIconColor, color);
// open the SpaceMenuMoreOptions menu
await openSpaceMenuMoreOptions(tester, space);
// 2. rename the space name
final renameOption = find.text(LocaleKeys.button_rename.tr());
await tester.tapButton(renameOption);
await tester.pumpUntilFound(find.byType(EditWorkspaceNameBottomSheet));
// input the new space name
final renameInputField = find.descendant(
of: find.byType(EditWorkspaceNameBottomSheet),
matching: find.byType(TextField),
);
const renameSpaceName = 'HelloWorld';
await tester.enterText(renameInputField, renameSpaceName);
await tester.pumpAndSettle();
await tester.tapButton(find.text(LocaleKeys.button_confirm.tr()));
// click the done button
await tester.pumpAndSettle();
final renameSuccess = find.text(
LocaleKeys.space_success_renameSpace.tr(),
);
await tester.pumpUntilNotFound(renameSuccess);
// check the space name is updated
await openSpaceMenu(tester);
final renameSpaceItem = find.descendant(
of: find.byType(MobileSpaceMenuItem),
matching: find.text(renameSpaceName),
);
expect(renameSpaceItem, findsOneWidget);
// 3. manage the space
await openSpaceMenuMoreOptions(tester, space);
final manageOption = find.text(LocaleKeys.space_manage.tr());
await tester.tapButton(manageOption);
await tester.pumpUntilFound(find.byType(ManageSpaceWidget));
// 3.1 rename the space
final textField = find.descendant(
of: find.byType(ManageSpaceWidget),
matching: find.byType(TextField),
);
await tester.enterText(textField, 'AppFlowy');
await tester.pumpAndSettle();
// 3.2 change the permission
final permissionOption2 = find.byType(ManageSpacePermissionOption);
await tester.tapButton(permissionOption2);
await tester.pumpAndSettle();
final publicOption = find.text(LocaleKeys.space_publicPermission.tr());
await tester.tapButton(publicOption);
await tester.pumpAndSettle();
// 3.3 change the icon
// change the space icon color
final color2 = builtInSpaceColors[2];
final iconOption2 = find.descendant(
of: find.byType(ManageSpaceIconOption),
matching: find.byWidgetPredicate(
(w) => w is SpaceColorItem && w.color == color2,
),
);
await tester.tapButton(iconOption2);
await tester.pumpAndSettle();
// change the space icon
final icon2 = kIconGroups![0].icons[2];
final iconItem2 = find.descendant(
of: find.byType(ManageSpaceIconOption),
matching: find.byWidgetPredicate(
(w) => w is SpaceIconItem && w.icon == icon2,
),
);
await tester.tapButton(iconItem2);
await tester.pumpAndSettle();
// click the done button
final doneButton2 = find.descendant(
of: find.byWidgetPredicate(
(w) =>
w is BottomSheetHeader &&
w.title == LocaleKeys.space_manageSpace.tr(),
),
matching: find.text(LocaleKeys.button_done.tr()),
);
await tester.tapButton(doneButton2);
await tester.pumpAndSettle();
// check the space is updated
final spaceItems2 = find.byType(MobileSpaceMenuItem);
final spaceWidget2 =
tester.widgetList<MobileSpaceMenuItem>(spaceItems2).last;
final space2 = spaceWidget2.space;
expect(space2.name, 'AppFlowy');
expect(space2.spacePermission, SpacePermission.publicToAll);
expect(space2.spaceIcon, icon2.iconPath);
expect(space2.spaceIconColor, color2);
final manageSuccess = find.text(
LocaleKeys.space_success_updateSpace.tr(),
);
await tester.pumpUntilNotFound(manageSuccess);
// 4. duplicate the space
await openSpaceMenuMoreOptions(tester, space);
final duplicateOption = find.text(LocaleKeys.space_duplicate.tr());
await tester.tapButton(duplicateOption);
final duplicateSuccess = find.text(
LocaleKeys.space_success_duplicateSpace.tr(),
);
await tester.pumpUntilNotFound(duplicateSuccess);
// check the space is duplicated
await openSpaceMenu(tester);
final spaceItems3 = find.byType(MobileSpaceMenuItem);
expect(spaceItems3, findsNWidgets(4));
// 5. delete the space
await openSpaceMenuMoreOptions(tester, space);
final deleteOption = find.text(LocaleKeys.button_delete.tr());
await tester.tapButton(deleteOption);
final confirmDeleteButton = find.descendant(
of: find.byType(CupertinoDialogAction),
matching: find.text(LocaleKeys.button_delete.tr()),
);
await tester.tapButton(confirmDeleteButton);
final deleteSuccess = find.text(
LocaleKeys.space_success_deleteSpace.tr(),
);
await tester.pumpUntilNotFound(deleteSuccess);
// check the space is deleted
final spaceItems4 = find.byType(MobileSpaceMenuItem);
expect(spaceItems4, findsNWidgets(3));
});
});
}
@@ -1,37 +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/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
@@ -1,6 +1,7 @@
import 'package:integration_test/integration_test.dart';
import 'page_style_test.dart' as page_style_test;
import 'plus_menu_test.dart' as plus_menu_test;
import 'title_test.dart' as title_test;
void main() {
@@ -9,4 +10,5 @@ void main() {
// Document integration tests
title_test.main();
page_style_test.main();
plus_menu_test.main();
}
@@ -1,42 +1,19 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/dir.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document page style', () {
group('document page style:', () {
double getCurrentEditorFontSize() {
final editorPage = find
.byType(AppFlowyEditorPage)
@@ -57,11 +34,9 @@ void main() {
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.height ??
PageStyleLineHeightLayout.normal.lineHeight;
.style()
.textStyleConfiguration
.lineHeight;
}
testWidgets('change font size in page style settings', (tester) async {
@@ -87,20 +62,24 @@ void main() {
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
var lineHeight = getCurrentEditorLineHeight();
expect(
getCurrentEditorLineHeight(),
lineHeight,
PageStyleLineHeightLayout.normal.lineHeight,
);
// change line height from normal to large
await tester.tapSvgButton(FlowySvgs.m_layout_large_s);
await tester.pumpAndSettle();
lineHeight = getCurrentEditorLineHeight();
expect(
getCurrentEditorLineHeight(),
lineHeight,
PageStyleLineHeightLayout.large.lineHeight,
);
// change line height from large to small
await tester.tapSvgButton(FlowySvgs.m_layout_small_s);
lineHeight = getCurrentEditorLineHeight();
expect(
getCurrentEditorLineHeight(),
lineHeight,
PageStyleLineHeightLayout.small.lineHeight,
);
});
@@ -0,0 +1,89 @@
import 'dart:async';
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_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document plus menu:', () {
testWidgets('add the toggle heading blocks via plus menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('toggle heading blocks');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// open the plus menu and select the toggle heading block
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_toggleHeading1.tr(),
);
// check the block is inserted
final block1 = editorState.getNodeAtPath([0])!;
expect(block1.type, equals(ToggleListBlockKeys.type));
expect(block1.attributes[ToggleListBlockKeys.level], equals(1));
// click the expand button won't cancel the selection
await tester.tapButton(find.byIcon(Icons.arrow_right));
expect(
editorState.selection,
equals(Selection.collapsed(Position(path: [0]))),
);
// focus on the next line
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [1])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// open the plus menu and select the toggle heading block
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_toggleHeading2.tr(),
);
// check the block is inserted
final block2 = editorState.getNodeAtPath([1])!;
expect(block2.type, equals(ToggleListBlockKeys.type));
expect(block2.attributes[ToggleListBlockKeys.level], equals(2));
// focus on the next line
await tester.pumpAndSettle();
// open the plus menu and select the toggle heading block
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_toggleHeading3.tr(),
);
// check the block is inserted
final block3 = editorState.getNodeAtPath([2])!;
expect(block3.type, equals(ToggleListBlockKeys.type));
expect(block3.attributes[ToggleListBlockKeys.level], equals(3));
// wait a few milliseconds to ensure the selection is updated
await Future.delayed(const Duration(milliseconds: 100));
// check the selection is collapsed
expect(
editorState.selection,
equals(Selection.collapsed(Position(path: [2]))),
);
});
});
}
@@ -1,39 +1,10 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.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/dir.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
@@ -1,47 +1,25 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/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';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('create new page', () {
group('create new page in home page:', () {
testWidgets('create document', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.local,
);
await tester.launchInAnonymousMode();
// tap the create page button
final createPageButton = find.byWidgetPredicate(
(widget) =>
widget is FlowySvg &&
widget.svg.path == FlowySvgs.m_home_unselected_m.path,
widget.svg.path == FlowySvgs.m_home_add_m.path,
);
await tester.tapButton(createPageButton);
await tester.pumpAndSettle();
expect(find.byType(MobileDocumentScreen), findsOneWidget);
});
});
@@ -1,33 +1,15 @@
// 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/mobile/presentation/home/home.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/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';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('anonymous sign in on mobile', () {
group('anonymous sign in on mobile:', () {
testWidgets('anon user and then sign in', (tester) async {
await tester.initializeAppFlowy();
await tester.launchInAnonymousMode();
// expect to see the home page
expect(find.byType(MobileHomeScreen), findsOneWidget);
@@ -1,15 +1,20 @@
import 'package:appflowy_backend/log.dart';
import 'package:integration_test/integration_test.dart';
import 'mobile/document/page_style_test.dart' as page_style_test;
import 'mobile/document/document_test_runner.dart' as document_test_runner;
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
Future<void> runIntegrationOnMobile() async {
Future<void> main() async {
Log.shared.disableLog = true;
await runIntegration1OnMobile();
}
Future<void> runIntegration1OnMobile() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main();
create_new_page_test.main();
page_style_test.main();
document_test_runner.main();
}
@@ -4,7 +4,12 @@ import 'desktop_runner_1.dart';
import 'desktop_runner_2.dart';
import 'desktop_runner_3.dart';
import 'desktop_runner_4.dart';
import 'mobile_runner.dart';
import 'desktop_runner_5.dart';
import 'desktop_runner_6.dart';
import 'desktop_runner_7.dart';
import 'desktop_runner_8.dart';
import 'desktop_runner_9.dart';
import 'mobile_runner_1.dart';
/// The main task runner for all integration tests in AppFlowy.
///
@@ -19,8 +24,13 @@ Future<void> main() async {
await runIntegration2OnDesktop();
await runIntegration3OnDesktop();
await runIntegration4OnDesktop();
await runIntegration5OnDesktop();
await runIntegration6OnDesktop();
await runIntegration7OnDesktop();
await runIntegration8OnDesktop();
await runIntegration9OnDesktop();
} else if (Platform.isIOS || Platform.isAndroid) {
await runIntegrationOnMobile();
await runIntegration1OnMobile();
} else {
throw Exception('Unsupported platform');
}
@@ -4,8 +4,10 @@ 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/mobile/presentation/base/type_option_menu_item.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart';
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
@@ -42,8 +44,8 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_platform/universal_platform.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'emoji.dart';
import 'util.dart';
@@ -193,8 +195,13 @@ extension CommonOperations on WidgetTester {
ViewLayoutPB layout = ViewLayoutPB.Document,
}) async {
final page = findPageName(name, layout: layout);
await tap(page, buttons: kSecondaryMouseButton);
await pumpAndSettle();
await hoverOnPageName(
name,
onHover: () async {
await tap(page, buttons: kSecondaryMouseButton);
await pumpAndSettle();
},
);
}
/// open the page with given name.
@@ -215,7 +222,7 @@ extension CommonOperations on WidgetTester {
of: find.byType(ViewMoreActionPopover),
matching: find.byFlowySvg(FlowySvgs.workspace_three_dots_s),
);
await tapButton(optionButton, warnIfMissed: true);
await tapButton(optionButton);
}
/// Tap the delete page button.
@@ -312,6 +319,15 @@ extension CommonOperations on WidgetTester {
await tapButton(shareButton);
}
// open the share menu and then click the publish tab
Future<void> openPublishMenu() async {
await tapShareButton();
final publishButton = find.textContaining(
LocaleKeys.shareAction_publishTab.tr(),
);
await tapButton(publishButton);
}
/// Tap the export markdown button
///
/// Must call [tapShareButton] first.
@@ -762,6 +778,69 @@ extension CommonOperations on WidgetTester {
await tap(button);
await pump();
}
Future<void> tapFileUploadHint() async {
final finder = find.byWidgetPredicate(
(w) =>
w is RichText &&
w.text.toPlainText().contains(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
),
);
await tap(finder);
await pumpAndSettle(const Duration(seconds: 2));
}
/// Create a new document on mobile
Future<void> createNewDocumentOnMobile(String name) async {
final createPageButton = find.byKey(
BottomNavigationBarItemType.add.valueKey,
);
await tapButton(createPageButton);
expect(find.byType(MobileDocumentScreen), findsOneWidget);
final title = editor.findDocumentTitle('');
expect(title, findsOneWidget);
final textField = widget<TextField>(title);
expect(textField.focusNode!.hasFocus, isTrue);
// input new name and press done button
await enterText(title, name);
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
final newTitle = editor.findDocumentTitle(name);
expect(newTitle, findsOneWidget);
expect(textField.controller!.text, name);
}
/// Open the plus menu
Future<void> openPlusMenuAndClickButton(String buttonName) async {
assert(
UniversalPlatform.isMobile,
'This method is only supported on mobile platforms',
);
final plusMenuButton = find.byKey(addBlockToolbarItemKey);
final addMenuItem = find.byType(AddBlockMenu);
await tapButton(plusMenuButton);
await pumpUntilFound(addMenuItem);
final toggleHeading1 = find.byWidgetPredicate(
(widget) =>
widget is TypeOptionMenuItem && widget.value.text == buttonName,
);
final scrollable = find.ancestor(
of: find.byType(TypeOptionGridView),
matching: find.byType(Scrollable),
);
await scrollUntilVisible(
toggleHeading1,
100,
scrollable: scrollable,
);
await tapButton(toggleHeading1);
await pumpUntilNotFound(addMenuItem);
}
}
extension SettingsFinder on CommonFinders {
@@ -790,6 +869,25 @@ extension SettingsFinder on CommonFinders {
.first;
}
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;
}
}
extension ViewLayoutPBTest on ViewLayoutPB {
String get menuName {
switch (this) {
@@ -55,7 +55,7 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/ti
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
@@ -596,7 +596,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
final banner = find.byType(RowBanner);
expect(banner, findsOneWidget);
await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse);
await startGesture(
getCenter(banner) + const Offset(0, -10),
kind: PointerDeviceKind.mouse,
);
await pumpAndSettle();
}
@@ -608,7 +611,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButtonWithName(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
await pumpAndSettle();
}
Future<void> openEmojiPicker() async =>
@@ -4,14 +4,15 @@ import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
import 'common_operations.dart';
@@ -117,4 +118,20 @@ extension AppFlowySettings on WidgetTester {
await tapAt(Offset.zero);
await pumpAndSettle();
}
Future<void> updateNamespace(String namespace) async {
final dialog = find.byType(DomainSettingsDialog);
expect(dialog, findsOneWidget);
// input the new namespace
await enterText(
find.descendant(
of: dialog,
matching: find.byType(TextField),
),
namespace,
);
await tapButton(find.text(LocaleKeys.button_save.tr()));
await pumpAndSettle();
}
}
@@ -40,9 +40,13 @@ extension AppFlowyWorkspace on WidgetTester {
moreButton,
onHover: () async {
await tapButton(moreButton);
await tapButton(
find.findTextInFlowyText(LocaleKeys.button_rename.tr()),
// wait for the menu to open
final renameButton = find.findTextInFlowyText(
LocaleKeys.button_rename.tr(),
);
await pumpUntilFound(renameButton);
expect(renameButton, findsOneWidget);
await tapButton(renameButton);
final input = find.byType(TextFormField);
expect(input, findsOneWidget);
await enterText(input, name);
@@ -1,9 +1,9 @@
import 'dart:async';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:flutter/services.dart';
class NetworkListener {
@@ -20,6 +20,7 @@ extension MobileRouter on BuildContext {
bool addInRecent = true,
bool showMoreButton = true,
String? fixedTitle,
String? blockId,
}) async {
// set the current view before pushing the new view
getIt<MenuSharedState>().latestOpenView = view;
@@ -32,6 +33,9 @@ extension MobileRouter on BuildContext {
if (fixedTitle != null) {
queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle;
}
if (blockId != null) {
queryParameters[MobileDocumentScreen.viewBlockId] = blockId;
}
}
final uri = Uri(
@@ -61,6 +61,7 @@ class NotificationReminderBloc
reminderContent: node.delta?.toPlainText() ?? '',
nodes: [node],
status: NotificationReminderStatus.loaded,
blockId: reminder.meta[ReminderMetaKeys.blockId],
),
);
}
@@ -205,6 +206,7 @@ class NotificationReminderState with _$NotificationReminderState {
@Default(NotificationReminderStatus.initial)
NotificationReminderStatus status,
@Default([]) List<Node> nodes,
String? blockId,
ViewPB? view,
}) = _NotificationReminderState;
@@ -30,6 +30,7 @@ class MobileViewPage extends StatefulWidget {
this.arguments,
this.fixedTitle,
this.showMoreButton = true,
this.blockId,
});
/// view id
@@ -38,6 +39,7 @@ class MobileViewPage extends StatefulWidget {
final String? title;
final Map<String, dynamic>? arguments;
final bool showMoreButton;
final String? blockId;
// only used in row page
final String? fixedTitle;
@@ -63,6 +65,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
@override
void dispose() {
_appBarOpacity.dispose();
// there's no need to remove the listener, because the observer will be disposed when the widget is unmounted.
// inside the observer, the listener will be removed automatically.
// _scrollNotificationObserver?.removeListener(_onScrollNotification);
_scrollNotificationObserver = null;
super.dispose();
@@ -177,6 +183,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
context: PluginContext(userProfile: state.userProfilePB),
data: {
MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
MobileDocumentScreen.viewBlockId: widget.blockId,
},
);
},
@@ -9,12 +9,14 @@ class TypeOptionMenuItemValue<T> {
required this.text,
required this.backgroundColor,
required this.onTap,
this.iconPadding,
});
final T value;
final FlowySvgData icon;
final String text;
final Color backgroundColor;
final EdgeInsets? iconPadding;
final void Function(BuildContext context, T value) onTap;
}
@@ -22,7 +24,7 @@ class TypeOptionMenu<T> extends StatelessWidget {
const TypeOptionMenu({
super.key,
required this.values,
this.width = 94,
this.width = 98,
this.iconWidth = 72,
this.scaleFactor = 1.0,
this.maxAxisSpacing = 18,
@@ -39,17 +41,18 @@ class TypeOptionMenu<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _GridView(
return TypeOptionGridView(
crossAxisCount: crossAxisCount,
mainAxisSpacing: maxAxisSpacing * scaleFactor,
itemWidth: width * scaleFactor,
children: values
.map(
(value) => _TypeOptionMenuItem<T>(
(value) => TypeOptionMenuItem<T>(
value: value,
width: width,
iconWidth: iconWidth,
scaleFactor: scaleFactor,
iconPadding: value.iconPadding,
),
)
.toList(),
@@ -57,18 +60,21 @@ class TypeOptionMenu<T> extends StatelessWidget {
}
}
class _TypeOptionMenuItem<T> extends StatelessWidget {
const _TypeOptionMenuItem({
class TypeOptionMenuItem<T> extends StatelessWidget {
const TypeOptionMenuItem({
super.key,
required this.value,
this.width = 94,
this.iconWidth = 72,
this.scaleFactor = 1.0,
this.iconPadding,
});
final TypeOptionMenuItemValue<T> value;
final double iconWidth;
final double width;
final double scaleFactor;
final EdgeInsets? iconPadding;
double get scaledIconWidth => iconWidth * scaleFactor;
double get scaledWidth => width * scaleFactor;
@@ -88,7 +94,8 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
borderRadius: BorderRadius.circular(24 * scaleFactor),
),
),
padding: EdgeInsets.all(21 * scaleFactor),
padding: EdgeInsets.all(21 * scaleFactor) +
(iconPadding ?? EdgeInsets.zero),
child: FlowySvg(
value.icon,
),
@@ -113,8 +120,9 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
}
}
class _GridView extends StatelessWidget {
const _GridView({
class TypeOptionGridView extends StatelessWidget {
const TypeOptionGridView({
super.key,
required this.children,
required this.crossAxisCount,
required this.mainAxisSpacing,
@@ -3,18 +3,24 @@ import 'dart:async';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.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/publish_name_generator.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/shared/error_code/error_code_map.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/navigator_context_exntesion.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -27,74 +33,94 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ViewPageBottomSheet(
view: view,
onAction: (action) async {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate());
return BlocListener<ShareBloc, ShareState>(
listener: (context, state) => _showToast(context, state),
child: BlocListener<ViewBloc, ViewState>(
listener: (context, state) {
if (state.successOrFailure.isSuccess && state.isDeleted) {
context.go('/home');
}
},
child: ViewPageBottomSheet(
view: view,
onAction: (action) async => _onAction(context, action),
onRename: (name) {
_onRename(context, name);
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
context.popToHome();
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
context.pop();
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.publish:
await _publish(context);
if (context.mounted) {
context.pop();
}
break;
case MobileViewBottomSheetBodyAction.unpublish:
_unpublish(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyPublishLink:
_copyPublishLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.visitSite:
_visitPublishedSite(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyShareLink:
_copyShareLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
_onRename(context, name);
context.pop();
},
},
),
),
);
}
Future<void> _onAction(
BuildContext context,
MobileViewBottomSheetBodyAction action,
) async {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
_duplicate(context);
break;
case MobileViewBottomSheetBodyAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
context.pop();
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
_addFavorite(context);
break;
case MobileViewBottomSheetBodyAction.removeFromFavorites:
_removeFavorite(context);
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.publish:
await _publish(context);
if (context.mounted) {
context.pop();
}
break;
case MobileViewBottomSheetBodyAction.unpublish:
_unpublish(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyPublishLink:
_copyPublishLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.visitSite:
_visitPublishedSite(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyShareLink:
_copyShareLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.updatePathName:
_updatePathName(context);
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
}
Future<void> _publish(BuildContext context) async {
final id = context.read<ShareBloc>().view.id;
final publishName = await generatePublishName(
id,
view.name,
final lastPublishName = context.read<ShareBloc>().state.pathName;
final publishName = lastPublishName.orDefault(
await generatePublishName(
id,
view.name,
),
);
if (context.mounted) {
context.read<ShareBloc>().add(
@@ -104,19 +130,44 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
[view.id],
),
);
showToastNotification(
context,
message: LocaleKeys.publish_publishSuccessfully.tr(),
);
}
}
void _duplicate(BuildContext context) {
context.read<ViewBloc>().add(const ViewEvent.duplicate());
context.pop();
showToastNotification(
context,
message: LocaleKeys.button_duplicateSuccessfully.tr(),
);
}
void _addFavorite(BuildContext context) {
_toggleFavorite(context);
showToastNotification(
context,
message: LocaleKeys.button_favoriteSuccessfully.tr(),
);
}
void _removeFavorite(BuildContext context) {
_toggleFavorite(context);
showToastNotification(
context,
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
);
}
void _toggleFavorite(BuildContext context) {
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
context.pop();
}
void _unpublish(BuildContext context) {
context.read<ShareBloc>().add(const ShareEvent.unPublish());
showToastNotification(
context,
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
);
}
void _copyPublishLink(BuildContext context) {
@@ -177,4 +228,112 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
}
void _updatePathName(BuildContext context) async {
final shareBloc = context.read<ShareBloc>();
final pathName = shareBloc.state.pathName;
await showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.shareAction_updatePathName.tr(),
showCloseButton: true,
showDragHandle: true,
showDivider: false,
padding: const EdgeInsets.symmetric(horizontal: 16),
builder: (bottomSheetContext) {
FlowyResult<void, FlowyError>? previousUpdatePathNameResult;
return EditWorkspaceNameBottomSheet(
type: EditWorkspaceNameType.edit,
workspaceName: pathName,
hintText: '',
validator: (value) => null,
validatorBuilder: (context) {
return BlocProvider.value(
value: shareBloc,
child: BlocBuilder<ShareBloc, ShareState>(
builder: (context, state) {
final updatePathNameResult = state.updatePathNameResult;
if (updatePathNameResult == null &&
previousUpdatePathNameResult == null) {
return const SizedBox.shrink();
}
if (updatePathNameResult != null) {
previousUpdatePathNameResult = updatePathNameResult;
}
final widget = previousUpdatePathNameResult?.fold(
(value) => const SizedBox.shrink(),
(error) => FlowyText(
error.code.publishErrorMessage.orDefault(
LocaleKeys.settings_sites_error_updatePathNameFailed
.tr(),
),
maxLines: 3,
fontSize: 12,
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
color: Theme.of(context).colorScheme.error,
),
) ??
const SizedBox.shrink();
return widget;
},
),
);
},
onSubmitted: (name) {
// rename the path name
Log.info('rename the path name, from: $pathName, to: $name');
shareBloc.add(ShareEvent.updatePathName(name));
},
);
},
);
shareBloc.add(const ShareEvent.clearPathNameResult());
}
void _showToast(BuildContext context, ShareState state) {
if (state.publishResult != null) {
state.publishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_publishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
type: ToastificationType.error,
),
);
} else if (state.unpublishResult != null) {
state.unpublishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishFailed.tr(),
description: error.msg,
type: ToastificationType.error,
),
);
} else if (state.updatePathNameResult != null) {
state.updatePathNameResult!.onSuccess(
(value) {
showToastNotification(
context,
message:
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
context.pop();
},
);
}
}
}
@@ -0,0 +1,96 @@
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart';
import 'package:appflowy/util/xfile_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pbenum.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class MobileMediaUploadSheetContent extends StatelessWidget {
const MobileMediaUploadSheetContent({super.key, required this.dialogContext});
final BuildContext dialogContext;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 12),
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: MobileFileUploadMenu(
onInsertLocalFile: (files) async {
dialogContext.pop();
await insertLocalFiles(
context,
files,
userProfile: context.read<MediaCellBloc>().state.userProfile,
documentId: context.read<MediaCellBloc>().rowId,
onUploadSuccess: (file, path, isLocalMode) {
final mediaCellBloc = context.read<MediaCellBloc>();
if (mediaCellBloc.isClosed) {
return;
}
mediaCellBloc.add(
MediaCellEvent.addFile(
url: path,
name: file.name,
uploadType: isLocalMode
? FileUploadTypePB.LocalFile
: FileUploadTypePB.CloudFile,
fileType: file.fileType.toMediaFileTypePB(),
),
);
},
);
},
onInsertNetworkFile: (url) async => _onInsertNetworkFile(
url,
dialogContext,
context,
),
),
);
}
Future<void> _onInsertNetworkFile(
String url,
BuildContext dialogContext,
BuildContext context,
) async {
dialogContext.pop();
if (url.isEmpty) return;
final uri = Uri.tryParse(url);
if (uri == null) {
return;
}
final fakeFile = XFile(uri.path);
MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB();
fileType =
fileType == MediaFileTypePB.Other ? MediaFileTypePB.Link : fileType;
String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : "";
if (name.isEmpty && uri.pathSegments.length > 1) {
name = uri.pathSegments[uri.pathSegments.length - 2];
} else if (name.isEmpty) {
name = uri.host;
}
context.read<MediaCellBloc>().add(
MediaCellEvent.addFile(
url: url,
name: name,
uploadType: FileUploadTypePB.NetworkFile,
fileType: fileType,
),
);
}
}
@@ -24,6 +24,7 @@ enum MobileViewBottomSheetBodyAction {
copyPublishLink,
visitSite,
copyShareLink,
updatePathName,
}
typedef MobileViewBottomSheetBodyActionCallback = void Function(
@@ -164,6 +165,15 @@ class MobileViewBottomSheetBody extends StatelessWidget {
final isPublished = context.watch<ShareBloc>().state.isPublished;
if (isPublished) {
return [
MobileQuickActionButton(
text: LocaleKeys.shareAction_updatePathName.tr(),
icon: FlowySvgs.view_item_rename_s,
iconSize: const Size.square(18),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.updatePathName,
),
),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.shareAction_visitSite.tr(),
icon: FlowySvgs.m_visit_site_s,
@@ -47,6 +47,7 @@ Future<T?> showMobileBottomSheet<T>(
Color? barrierColor,
double? elevation,
bool showDoneButton = false,
void Function(BuildContext context)? onDone,
bool enableDraggableScrollable = false,
bool enableScrollable = false,
// this field is only used if showDragHandle is true
@@ -112,6 +113,7 @@ Future<T?> showMobileBottomSheet<T>(
showRemoveButton: showRemoveButton,
title: title,
onRemove: onRemove,
onDone: onDone,
),
);
@@ -143,6 +145,7 @@ Future<T?> showMobileBottomSheet<T>(
) ??
Expanded(
child: Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: child,
@@ -207,6 +210,7 @@ class BottomSheetHeader extends StatelessWidget {
required this.title,
required this.showDoneButton,
this.onRemove,
this.onDone,
});
final bool showBackButton;
@@ -215,6 +219,7 @@ class BottomSheetHeader extends StatelessWidget {
final String title;
final bool showDoneButton;
final VoidCallback? onRemove;
final void Function(BuildContext context)? onDone;
@override
Widget build(BuildContext context) {
@@ -256,7 +261,13 @@ class BottomSheetHeader extends StatelessWidget {
Align(
alignment: Alignment.centerRight,
child: BottomSheetDoneButton(
onDone: () => Navigator.pop(context),
onDone: () {
if (onDone != null) {
onDone?.call(context);
} else {
Navigator.pop(context);
}
},
),
),
],
@@ -11,12 +11,16 @@ class OptionTextField extends StatelessWidget {
const OptionTextField({
super.key,
required this.controller,
this.autoFocus = false,
required this.isPrimary,
required this.fieldType,
required this.onTextChanged,
required this.onFieldTypeChanged,
});
final TextEditingController controller;
final bool autoFocus;
final bool isPrimary;
final FieldType fieldType;
final void Function(String value) onTextChanged;
final void Function(FieldType value) onFieldTypeChanged;
@@ -25,10 +29,14 @@ class OptionTextField extends StatelessWidget {
Widget build(BuildContext context) {
return FlowyOptionTile.textField(
controller: controller,
autofocus: autoFocus,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),
onTextChanged: onTextChanged,
leftIcon: GestureDetector(
onTap: () async {
if (isPrimary) {
return;
}
final fieldType = await showFieldTypeGridBottomSheet(
context,
title: LocaleKeys.grid_field_editProperty.tr(),
@@ -5,9 +5,8 @@ import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -71,71 +70,52 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
);
}
Widget _buildDatePicker() => MultiBlocProvider(
providers: [
BlocProvider<DateCellEditorBloc>(
create: (_) => DateCellEditorBloc(
reminderBloc: getIt<ReminderBloc>(),
cellController: widget.controller,
),
),
],
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
return MobileAppFlowyDatePicker(
selectedDay: state.dateTime,
dateStr: state.dateStr,
endDateStr: state.endDateStr,
timeStr: state.timeStr,
endTimeStr: state.endTimeStr,
startDay: state.startDay,
endDay: state.endDay,
enableRanges: true,
isRange: state.isRange,
includeTime: state.includeTime,
use24hFormat: state.dateTypeOptionPB.timeFormat ==
TimeFormatPB.TwentyFourHour,
timeFormat: state.dateTypeOptionPB.timeFormat,
selectedReminderOption: state.reminderOption,
onStartTimeChanged: (String? time) {
if (time != null) {
context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setTime(time));
}
},
onEndTimeChanged: (String? time) {
if (time != null) {
context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setEndTime(time));
}
},
onDaySelected: (selectedDay, focusedDay) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.selectDay(selectedDay)),
onRangeSelected: (start, end, focused) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.selectDateRange(start, end)),
onRangeChanged: (value) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIsRange(value)),
onIncludeTimeChanged: (value) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIncludeTime(value)),
onClearDate: () => context
.read<DateCellEditorBloc>()
.add(const DateCellEditorEvent.clearDate()),
onReminderSelected: (option) =>
context.read<DateCellEditorBloc>().add(
DateCellEditorEvent.setReminderOption(
option: option,
selectedDay:
state.dateTime == null ? DateTime.now() : null,
),
),
);
},
),
);
Widget _buildDatePicker() {
return BlocProvider(
create: (_) => DateCellEditorBloc(
reminderBloc: getIt<ReminderBloc>(),
cellController: widget.controller,
),
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
final dateCellBloc = context.read<DateCellEditorBloc>();
return MobileAppFlowyDatePicker(
dateTime: state.dateTime,
endDateTime: state.endDateTime,
isRange: state.isRange,
includeTime: state.includeTime,
dateFormat: state.dateTypeOptionPB.dateFormat,
timeFormat: state.dateTypeOptionPB.timeFormat,
reminderOption: state.reminderOption,
onDaySelected: (selectedDay) {
dateCellBloc.add(DateCellEditorEvent.updateDateTime(selectedDay));
},
onRangeSelected: (start, end) {
dateCellBloc.add(DateCellEditorEvent.updateDateRange(start, end));
},
onIsRangeChanged: (value, dateTime, endDateTime) {
dateCellBloc.add(
DateCellEditorEvent.setIsRange(value, dateTime, endDateTime),
);
},
onIncludeTimeChanged: (value, dateTime, endDateTime) {
dateCellBloc.add(
DateCellEditorEvent.setIncludeTime(
value,
dateTime,
endDateTime,
),
);
},
onClearDate: () {
dateCellBloc.add(const DateCellEditorEvent.clearDate());
},
onReminderSelected: (option) {
dateCellBloc.add(DateCellEditorEvent.setReminderOption(option));
},
);
},
),
);
}
}
@@ -21,15 +21,15 @@ import 'mobile_quick_field_editor.dart';
const mobileSupportedFieldTypes = [
FieldType.RichText,
FieldType.Number,
FieldType.URL,
FieldType.SingleSelect,
FieldType.MultiSelect,
FieldType.DateTime,
FieldType.LastEditedTime,
FieldType.CreatedTime,
FieldType.Media,
FieldType.URL,
FieldType.Checkbox,
FieldType.Checklist,
FieldType.Media,
FieldType.LastEditedTime,
FieldType.CreatedTime,
// FieldType.Time,
];
@@ -224,7 +224,9 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
const _Divider(),
OptionTextField(
controller: controller,
autoFocus: widget.mode == FieldOptionMode.add,
fieldType: values.type,
isPrimary: widget.isPrimary,
onTextChanged: (value) {
isFieldNameChanged = true;
_updateOptionValues(name: value);
@@ -76,6 +76,7 @@ class _QuickEditFieldState extends State<QuickEditField> {
const VSpace(16),
OptionTextField(
controller: controller,
isPrimary: state.field.isPrimary,
fieldType: state.field.fieldType,
onTextChanged: (text) {
context
@@ -13,7 +13,6 @@ import 'package:appflowy/plugins/database/widgets/setting/field_visibility_exten
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -79,14 +78,12 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
viewId: viewId,
fieldController: databaseController.fieldController,
fieldInfo: field,
index: index,
showTopBorder: false,
),
)
.toList();
return ReorderableListView.builder(
padding: EdgeInsets.zero,
proxyDecorator: (_, index, anim) {
final field = fields[index];
return AnimatedBuilder(
@@ -103,7 +100,6 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
viewId: viewId,
fieldController: databaseController.fieldController,
fieldInfo: field,
index: index,
showTopBorder: true,
),
),
@@ -121,43 +117,32 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
},
header: firstCell,
footer: canCreate
? Column(
mainAxisSize: MainAxisSize.min,
children: [
_divider(),
_NewDatabaseFieldTile(viewId: viewId),
VSpace(
context.bottomSheetPadding(
ignoreViewPadding: false,
),
),
],
? Padding(
padding: const EdgeInsets.only(top: 20),
child: _NewDatabaseFieldTile(viewId: viewId),
)
: VSpace(
context.bottomSheetPadding(ignoreViewPadding: false),
),
: null,
itemCount: cells.length,
itemBuilder: (context, index) => cells[index],
padding: EdgeInsets.only(
bottom: context.bottomSheetPadding(ignoreViewPadding: false),
),
);
},
),
);
}
Widget _divider() => const VSpace(20);
}
class DatabaseFieldListTile extends StatelessWidget {
const DatabaseFieldListTile({
super.key,
this.index,
required this.fieldInfo,
required this.viewId,
required this.fieldController,
required this.showTopBorder,
});
final int? index;
final FieldInfo fieldInfo;
final String viewId;
final FieldController fieldController;
@@ -172,6 +157,7 @@ class DatabaseFieldListTile extends StatelessWidget {
fieldInfo: fieldInfo,
dimension: 20,
),
onTap: () => showEditFieldScreen(context, viewId, fieldInfo),
showTopBorder: showTopBorder,
);
} else {
@@ -1054,34 +1054,55 @@ class _SelectOptionFilterContentEditorState
}
}
class _DateTimeFilterContentEditor extends StatelessWidget {
class _DateTimeFilterContentEditor extends StatefulWidget {
const _DateTimeFilterContentEditor({
required this.filter,
});
final DateTimeFilter filter;
@override
State<_DateTimeFilterContentEditor> createState() =>
_DateTimeFilterContentEditorState();
}
class _DateTimeFilterContentEditorState
extends State<_DateTimeFilterContentEditor> {
late DateTime focusedDay;
bool get isRange => widget.filter.condition.isRange;
@override
void initState() {
super.initState();
focusedDay = (isRange ? widget.filter.start : widget.filter.timestamp) ??
DateTime.now();
}
@override
Widget build(BuildContext context) {
final isRange = filter.condition.isRange;
return MobileDatePicker(
isRange: isRange,
selectedDay: isRange ? filter.start : filter.timestamp,
startDay: isRange ? filter.start : null,
endDay: isRange ? filter.end : null,
onDaySelected: (selectedDay, _) {
selectedDay: isRange ? widget.filter.start : widget.filter.timestamp,
startDay: isRange ? widget.filter.start : null,
endDay: isRange ? widget.filter.end : null,
focusedDay: focusedDay,
onDaySelected: (selectedDay) {
final newFilter = isRange
? filter.copyWithRange(start: selectedDay, end: null)
: filter.copyWithTimestamp(timestamp: selectedDay);
? widget.filter.copyWithRange(start: selectedDay, end: null)
: widget.filter.copyWithTimestamp(timestamp: selectedDay);
context.read<MobileFilterEditorCubit>().updateFilter(newFilter);
},
onRangeSelected: (start, end, _) {
final newFilter = filter.copyWithRange(
onRangeSelected: (start, end) {
final newFilter = widget.filter.copyWithRange(
start: start,
end: end,
);
context.read<MobileFilterEditorCubit>().updateFilter(newFilter);
},
onPageChanged: (focusedDay) {
setState(() => this.focusedDay = focusedDay);
},
);
}
}
@@ -9,6 +9,7 @@ class MobileDocumentScreen extends StatelessWidget {
this.title,
this.showMoreButton = true,
this.fixedTitle,
this.blockId,
});
/// view id
@@ -16,12 +17,14 @@ class MobileDocumentScreen extends StatelessWidget {
final String? title;
final bool showMoreButton;
final String? fixedTitle;
final String? blockId;
static const routeName = '/docs';
static const viewId = 'id';
static const viewTitle = 'title';
static const viewShowMoreButton = 'show_more_button';
static const viewFixedTitle = 'fixed_title';
static const viewBlockId = 'block_id';
@override
Widget build(BuildContext context) {
@@ -31,6 +34,7 @@ class MobileDocumentScreen extends StatelessWidget {
viewLayout: ViewLayoutPB.Document,
showMoreButton: showMoreButton,
fixedTitle: fixedTitle,
blockId: blockId,
);
}
}
@@ -279,16 +279,10 @@ class _HomePageState extends State<_HomePage> {
ToastificationType toastType = ToastificationType.success;
switch (actionType) {
case UserWorkspaceActionType.open:
message = result.fold(
(s) {
toastType = ToastificationType.success;
return LocaleKeys.workspace_openSuccess.tr();
},
(e) {
toastType = ToastificationType.error;
return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}';
},
);
message = result.onFailure((e) {
toastType = ToastificationType.error;
return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}';
});
break;
case UserWorkspaceActionType.delete:
message = result.fold(
@@ -0,0 +1,3 @@
class SpaceUIConstants {
static const itemHeight = 52.0;
}
@@ -0,0 +1,53 @@
import 'package:appflowy/shared/icon_emoji_picker/icon.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart' hide Icon;
import 'widgets.dart';
enum ManageSpaceType {
create,
edit,
}
class ManageSpaceWidget extends StatelessWidget {
const ManageSpaceWidget({
super.key,
required this.controller,
required this.permission,
required this.selectedColor,
required this.selectedIcon,
required this.type,
});
final TextEditingController controller;
final ValueNotifier<SpacePermission> permission;
final ValueNotifier<String> selectedColor;
final ValueNotifier<Icon?> selectedIcon;
final ManageSpaceType type;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ManageSpaceNameOption(
controller: controller,
type: type,
),
ManageSpacePermissionOption(permission: permission),
ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 560,
),
child: ManageSpaceIconOption(
selectedColor: selectedColor,
selectedIcon: selectedIcon,
),
),
const VSpace(60),
],
);
}
}
Loaded 100 of 829 files, more files were not shown because too many files have changed in this diff. Show more