Compare commits

...
236 Commits
Author SHA1 Message Date
Lucas 05c1924940 chore: update CHANGELOG.md (#7209)
* Revert "fix: disable deleting mutilple nodes in table"

This reverts commit 0507c39863.

* chore: bump version 0.8.1

* chore: remove unused tests
2025-01-14 21:04:00 +08:00
FakhriAzzouz b2f3f902b2 chore: update ar-SA translations (#7210)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-14 19:59:11 +08:00
Richard Shiue 33fbba18ca fix: message scrolling (#7207) 2025-01-14 16:37:13 +08:00
Nathan.fooo cea3c69239 chore: update plan desc (#7208) 2025-01-14 16:29:42 +08:00
Morn ff91c0a909 fix: correct the reference error of the icon widget (#7203) 2025-01-14 16:08:01 +08:00
Richard Shiue d1efda4c50 fix: launch review 0.8.1 (#7185)
* fix: launch review

* chore: scroll to end upon sending new message

* chore: bump editor version

* chore: scroll to bottom after adding message

* chore: code reorg

* chore: bump editor version

* chore: bump editor ver

* chore: bump editor

* chore: bump editor

* fix: file block node insertion

* fix: do the same thing on image

* chore: update icons and translations
2025-01-14 12:35:51 +08:00
Morn 2b1d1ba2f4 chore: remove iconContent from IconsData (#7199) 2025-01-14 09:31:43 +08:00
Richard Shiue 9a237b5f18 chore: allow customize that forces distribute column widths (#7198)
* chore: allow customize that forces distribute column widths

* fix: missing intrinsic width

* fix: what am i doing
2025-01-13 18:47:45 +08:00
Lucas fc21d1d245 fix: retry count should be clear if the value exceeds max retries (#7195)
* fix: retry count should be clear if the value exceeds max retries

* feat: add retry button when loading image failed
2025-01-13 17:17:05 +08:00
Morn 0c6d4df14b fix: icons not supported in callout(#7192) (#7197) 2025-01-13 17:14:30 +08:00
Morn 8df5d8dcb4 fix: icons not display in search results (#7189)
* fix: icons not display in search results

* chore: clean code
2025-01-13 14:35:29 +08:00
Lucas aa9f285f59 fix: search results were filtered accidentally (#7196) 2025-01-13 14:34:23 +08:00
Lucas 6173022a15 fix: 0.8.1 launch review issues (#7186)
* fix: Search result title appears empty

* chore: change "Type to search" to "Search or ask a question…"

* fix: the toolbar shouldn’t over the search panel

* feat: add hint text next to the web url

* test: the toolbar shouldn’t over the search panel

* test: search result title appears empty
2025-01-13 09:22:04 +08:00
Nathan.fooo afb479607b chore: bump client api (#7194) 2025-01-12 23:32:44 +08:00
Nathan.fooo 342c361184 chore: ai block with context (#7193)
* chore: ai block with context

* chore: fix test
2025-01-12 22:33:21 +08:00
Nathan.foooandLucas.Xu 790d5612f7 chore: retry load image (#7179)
* chore: retry load image

* feat: support retry count and retry duration in network image

* chore: use loading builder in network image

* feat: support retry logic in network image

* feat: disable image menu when loading image

* chore: error prompt

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-01-10 09:43:18 +08:00
Morn 99a4e330e8 feat: enable to reorder favorites (#7172) 2025-01-09 14:33:53 +08:00
Richard Shiue c3b702849f chore: remove error message when sending a new message (#7177) 2025-01-09 13:49:37 +08:00
Lucas a49acd9220 chore: bump version 0.8.1 (#7178) 2025-01-09 12:04:42 +08:00
Lucas dd3b4dc5fb feat: resize table column width on mobile (#7174)
* feat: support resizing column width of table on mobile

* feat: support highlighting border when resizing

* test: support resizing column width of table on mobile
2025-01-09 11:37:43 +08:00
Morn 155817a0f1 fix: navigation bar issue on linked pages(#7111) (#7142) 2025-01-09 11:27:37 +08:00
Morn 89c4629ec2 feat: support auto text direction on mobile (#7092) (#7163) 2025-01-09 11:26:59 +08:00
Changho Park 6ca81b3a66 chore: update translations for ko-KR (#7161)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:11:05 +08:00
FakhriAzzouz b278e14ffd chore: update ar-SA translations (#7164)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:07:38 +08:00
dawn cb6f7cb4b6 chore: update fr-CA translations (#7166)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:07:12 +08:00
mizou-soga f94feb780a chore: update ja-JP translations (#7167)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:07:01 +08:00
Lucas 0ef1b27ae5 fix: filter the search results by duplicate ids (#7173) 2025-01-08 19:33:24 +08:00
Lucas 64994b3336 chore: enable AI seach (#7169)
* chore: enable AI seach

* chore: remove unused code

* fix: replace the old web base url with the new one
2025-01-08 15:52:24 +08:00
Nathan.foooandLucas.Xu 6966b303ff chore: bump collab commit id (#7168)
* chore: bump collab commit id

* chore: bump collab version

* chore: bump collab version

* fix: disable deleting mutilple nodes in table

* chore: bump collab version

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-01-08 14:46:44 +08:00
ab8e01bbf7 feat(flutter): pre-defined response formats (#7128)
* feat: pre-defined response formats

* chore: adjust bottom sheet

* chore: rename and clean up enums

* chore: move all mobile input actions to the bottom

* chore: bump client-api

* chore: connect to API

* chore: apply suggestions from code review

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

* chore: code cleanup

* chore: code cleanup

* chore: update client-api

* chore: expand page

* chore: simplify logic for not displaying related questions

* chore: remove hover effect view icon in select sources

* chore: regenerate with different format

* chore: remove error messages when sending new one

* chore: code style

* chore: bump client api

* fix: image not displaying and hide editing options

* chore: don't fetch related questions for image only

* chore: fix clippy

* chore: don't add related questions on regenerate

* chore: bump editor

* fix: expand sidebar page

* chore: update client api

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: weidong fu <nathan@appflowy.io>
2025-01-08 10:43:03 +08:00
Richard Shiue e25633636b fix(flutter_mobile): linked grid cannot be displayed in document (#7162) 2025-01-07 17:56:17 +08:00
Lucas d87a209c4a fix: context menu should not auto show when collaborating (#7151)
* fix: context menu should not auto show when collaborating

* chore: update document_bloc.dart
2025-01-07 09:40:20 +08:00
Lucas a33cf1f488 fix: filter out the space when opening new tab (#7152)
* fix: filter out the space when opening tab

* test: filter out the space when opening tab
2025-01-06 17:10:18 +08:00
Lucas 91236006d4 feat: custom share url, publish url and copy link to share (#7061)
* feat: custom share url, publish url and copy link to share

* chore: update translation

* feat: support base share domain

* feat: customize web url in login page

* feat: support customizing web url on mobile

* test: change web url test

* fix: cloud integration test

* fix: integration test

* fix: integration test
2025-01-06 16:05:05 +08:00
Richard Shiue fd9baf7a06 chore: code cleanup (#7150) 2025-01-06 13:34:11 +08:00
Mathias Mogensen ed646ccba2 chore: add loose restrictions to date time text field (#7024)
* chore: add loose restrictions to date time text field

* chore: remove date formatter
2025-01-05 21:02:29 +08:00
Lucas 7bc358d7ac fix: simple table issues and locale issues (#7138)
* fix: can't make changes on row or column of table

* fix: fallback to en-US if the locale is invalid

* chore: remove unused code

* fix: simple table issues
2025-01-05 19:55:15 +08:00
Morn 14fc287071 fix: recent icon doesn't work in space icon (#7133) (#7137) 2025-01-05 19:54:59 +08:00
Morn cdf7dced8b fix: count title towards word count(#7042) (#7127) 2025-01-05 19:54:46 +08:00
Lucas 552c59218c fix: convert false value in attributes to null (#7135) 2025-01-03 15:55:25 +08:00
Richard Shiue c7e0e36902 feat(flutter_desktop): save ai message to page improvements (#7136)
* chore: expand current space in select sources or when saving to a page

* chore: hide non-document views while saving to page

* chore: add buttons when saving to page

* chore: adjust icon color

* chore: adjust translations

* chore: code cleanup
2025-01-03 14:01:19 +08:00
FakhriAzzouz 1f82c6682b chore: update ar-SA translations (#7129)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-03 10:12:30 +08:00
Richard Shiue 21261ee813 feat(flutter_mobile): adjust bottom sheet quick action button style (#7121)
* feat: adjust bottom sheet quick action button style

* chore: remove final dividers
2025-01-03 10:11:17 +08:00
Morn 15deb8ea79 fix: icon picker issues on mobile (#7113)
* fix: error displaying in Page style

* fix: error displaying in Favorite/Recent page

* fix: complete the filter logic of icon picker

* fix: the color picker showed when tapping down

* fix: icons are not supported in subpage blocks

* chore: add some tests

* fix: recent icons not working for grid header icon
2025-01-03 10:04:14 +08:00
Kilu.He f7f99a162e chore: remove web app (#7126)
* feat: support rename workspace on web

* chore: remove web app from repo
2025-01-03 09:24:45 +08:00
Richard ShiueandMathias Mogensen c05f19edd2 feat(flutter_desktop): add button to expand side peek (#7118)
* chore: update sidebar top bar color

* feat(flutter_desktop): button to expand side peek

* Update frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart

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

* chore: fix comment

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2025-01-02 15:51:40 +08:00
Richard Shiue 407e88b27f chore: bump dependencies (#7123)
* chore: bump dependencies

* test: fix unit test

* fix: downgrade percent indicator

* chore: flutter analyze
2025-01-02 15:19:00 +08:00
Kilu.He 512113877b feat: support create workspace on web (#7122) 2025-01-02 10:46:56 +08:00
Lucas 8f7cb50dd4 fix: simple table issues on mobile (#7115)
* fix: header row/column tap areas are too small on mobile

* test: header row/column tap areas are too small on mobile

* feat: enable auto scroll after inserting column or row

* fix: enter after emoji will create a softbreak on mobile

* fix: header row/column tap areas are too small on mobile

* fix: simple table alignment not work for item that wraps

* test: simple table alignment not work for item that wraps
2024-12-31 16:01:45 +08:00
Ahad PatelandLucas.Xu 7dedb84504 feat: i18n for duplicated field names (#6769)
* feat: i18n for duplicated field names

* fix: flutter analyze

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-31 15:54:02 +08:00
Lucas b2ca5c77f6 fix: simple tests on mobile (#7102)
* fix: simple tests on mobile

* fix: subpage block padding
2024-12-31 12:03:44 +08:00
Darío HereñúandLucas.Xu 11d720465b chore: update es-VE translations (#3645)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-31 11:04:37 +08:00
FakhriAzzouz c2643bfb6c chore: update ar-SA translations (#7099)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-12-31 11:02:16 +08:00
Morn 3d4d4cf709 test: add edit link menu test (#7097) 2024-12-31 09:46:13 +08:00
Ahad PatelandLucas.Xu b965a5f3ae feat: delete the previous image when the cover changes in local mode (#6368)
* remove unecessary images from localstorage

* feat: Add handler for deleting previous cover image on cover image change

* fix: add local image case for versions after 0.5.5

* fix: add try catch block and delete action to bottom of function

* chore: add test case for uploading and deleting image in localmode

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-31 09:36:14 +08:00
KD-MM2andNathan.fooo 73463cd7e3 chore(i18n): update vi-VN translations Fink 🐦 (#6290)
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-12-30 18:38:34 +08:00
Richard Shiue 6158954d97 test: fix change server integeration test (#7096) 2024-12-30 18:10:55 +08:00
Kilu.He 3190eebf6e fix: modified universal link (#7094) 2024-12-30 18:10:36 +08:00
Lucas dfe994b341 feat: auto-dismiss collapsed handle on Android if no interaction occurs (#7088)
* feat: support auto-dismiss collapsed handle on Android

* fix: hit test area of collasepd handle is too big

* chore: upgrade appflowy_editor

* fix: simple table issues on mobile

* feat: highlight cell after insertion

* test: text color and cell background color test

* fix: sign_in_page_settings_test
2024-12-30 17:56:43 +08:00
Richard ShiueandLucas 92722d0922 fix(flutter_desktop): workspace menu ui issues (#7091)
* fix(flutter_desktop): remove log out and workspace option popovers conflict

* test: add integration test

* fix(flutter_desktop): workspace list scrollbar overlaps with list

* chore(flutter_desktop): fix padding around import from notion button

* chore(flutter_desktop): adjust popover conflict rules for workspace

* test: add integration tests

* chore(flutter_desktop): make the popoovers as barriers

* fix: regression from making the workspace item menu as barrier

* chore: update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart

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

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-12-30 17:55:40 +08:00
Lucas e04c1bdaec chore: update changelog (#7095) 2024-12-30 17:53:55 +08:00
Richard Shiue 20b16cf174 feat: add toast messages for ai chat interactions (#7086) 2024-12-30 17:17:36 +08:00
Morn 5ffa27f545 fix: toolbar menu not showing beacase keyboard height is not updated in time (#7060) 2024-12-30 16:57:59 +08:00
Morn 00cdbe5a1c fix: issues related to the emoji icon picker (#7063)
* fix: remove the scrolling conflict of the icon picker on macOS

* fix: the icon is not supported in sites tab

* feat: keep the icon panel open after click ramdom

* feat: the type of selector opened depends on the already set icon or emoji

* feat: the skin tone of the random emoji follows the selected skin ton

* fix: unit testing error
2024-12-30 16:42:14 +08:00
Richard Shiue dffb865a66 chore(flutter_desktop): adjust toast style (#7083) 2024-12-30 16:35:41 +08:00
Kilu.He 5e581e912f fix: hover controls position (#7093) 2024-12-30 15:51:58 +08:00
Kilu.He 256d015967 fix: add tip for database page (#7090) 2024-12-30 15:46:39 +08:00
Nathan.fooo d9b3f3f6c6 chore: remove tarui (#7089)
* chore: remove tarui

* chore: remove tarui config
2024-12-30 15:41:23 +08:00
Nathan.foooandLucas.Xu 09fa75f5ec chore: remove crates (#7085)
* chore: remove crate

* chore: use rust 1.81

* chore: remove build flag

* fix: macos build error

* chore: upgrade super_clipboard

* chore: fix pb lint warning

* chore: fix clippy

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-30 14:25:05 +08:00
Lucas 38c2937f64 fix: docker ci (#7087) 2024-12-30 13:35:37 +08:00
Richard Shiue 06d5bc734b fix: try to fix message regeneration being appended instead of replaced (#7084) 2024-12-30 12:38:56 +08:00
Nathan.fooo a521541cb7 chore: only sync when doucment was changed (#7081)
* chore: only sync when doucment was changed

* chore: fmt
2024-12-30 10:26:06 +08:00
Kilu.He 7d61252e6a feat: project management & document editor features on web (#7051)
* fix: support page operations

* fix: support select language of code block

* fix: support hover controls of document

* fix: support slash panel of document

* feat: support image block and file block of document

* feat: support outline block of document

* feat: support add document on document

* feat: support create sub-page on document

* feat: support mention panel

* feat: support more actions

* feat: support create space

* fix: add some shortcuts to document

* fix: add some shortcuts to document

* fix: multiple select blocks

* fix: reupdate trash after restore and delete

* fix: lint check

* fix: callout block

* fix: view icon

* fix: colors

* fix: colors

* feat: support add grid/board/calendar

* feat: support database new layout

* fix: slash panel

* fix: bugs

* fix: grid table

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: bugs

* fix: some bugs

* fix: some bugs

* fix: invite members

* fix: styles

* fix: styles

* fix: styles

* fix: inline code

* fix: image block

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: add applink

* fix: add applink

* fix: add applink

* fix: add share link

* fix: inline link

* fix: add log

* fix: simple table bugs

* fix: simple table bugs

* fix: some bugs

* fix: build size

* fix: build size

* fix: remove private from package.json

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: some bugs

* fix: support paste md

* fix: modified toolbar

* fix: modified toolbard actions

* fix: adjust editor toolbar

* fix: support billing
2024-12-30 10:12:54 +08:00
Lucas cb2b9ac537 fix: simple table issues on mobile (#7068)
* feat: use fixed height for table menu

* feat: do not close menu after enabling header column/row

* fix: remove cell background color

* fix: the text color doesn't apply to the heading block

* fix: replace quick action icon color

* fix: add done button in text color menu

* fix: don't access clipboard when opening table action menu

* fix: table navigation issues and reminder issue

* fix: cursor flash when create a row and column at the same time

* fix: unable to insert page sometimes

* fix: use default name if the title is empty

* chore: replace align icons
2024-12-30 09:23:03 +08:00
Alexandre Cirilo af31f8cf6f chore(i18n): update fr-FR translations 🐦 (#7076) 2024-12-30 09:16:52 +08:00
Richard Shiue 87c1245a3f fix: launch review issues 0.7.9 (#7073)
* fix: disable select sources when on local AI

* chore: tooltips for right sidebar actions

* chore: update selection after inserting into existing page

* fix: mobile select sources button background color

* fix: ai response metadata

* chore: don't update selected sources section until reopen

* chore: show views from all spaces in select sources menu

* chore: revert podfile changes

* chore: decrease spacing between metadata and hover actions

* chore: improve placeholder for ai message metadata
2024-12-29 21:48:37 +08:00
Nathan.fooo 49d9417cac refactor: crate dir (#7080) 2024-12-29 14:47:28 +08:00
jumbi77 1f29235538 chore(i18n): update de-DE translations 🐦 (#7071) 2024-12-28 15:39:34 +08:00
Richard Shiue fd1e36b21a fix: numbered list misalignment (#7066)
* fix: numbered list misalignment

* chore: fixed width numbers
2024-12-27 16:08:27 +08:00
Lucas 7db11c7cfd chore: fix release builder issue (#7058)
* chore: use --locked to install cargo-make

* chore: upgrade to macos 13
2024-12-26 14:03:23 +08:00
Lucas 33888d583b chore: bump version 0.7.9 (#7056) 2024-12-26 13:49:55 +08:00
3959cdba3a feat: add option to paste plain text (#7045)
* feat: add option to paste plain text

* refactor: optimize the code

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: Lucas <lucas.xu@appflowy.io>

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-12-26 13:22:27 +08:00
Richard ShiueandLucas 200b367e4c feat: add ai message content to document (#7041)
* feat: add ai response content to page

* chore: apply suggestions from code review

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

* chore: apply suggestions from code review

* chore: reorganize code

* chore: i18n

* chore: enable opening the document in the sidebar

* fix: async await

* chore: rename ai message action bar widget

* feat: make transactions be reflected in the opened document

* chore: don't forget to close the bloc

* fix: isLastLineEmpty

* chore: code cleanup

* fix: sync after EditorState.apply

* chore: decrease visibility of DocumentBlocMap

* chore: add back missing assert

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-12-26 12:18:48 +08:00
Richard Shiue 956d2dfd07 fix(flutter_desktop): row detail checklist cell "create new" text field doesn't clear after clicking on create button (#7055)
* fix: clear text controller when clicking on add button no matter what

* test: add integration test
2024-12-26 12:18:17 +08:00
Lucas 83e50d376e feat: support plus menu in table cell on mobile (#7048)
* feat: support plus menu in table cell on mobile

* test: support plus menu in table cell on mobile

* feat: add lightImpact feedback

* chore: optimize the action sheet
2024-12-26 11:09:56 +08:00
Mathias Mogensen 802a667907 feat: drag view into document to link to view (#6991)
* feat: drag view into document

* chore: remove redundant method call
2024-12-26 09:57:01 +08:00
e67e9cc647 feat: support recent section in icon picker (#7015)
* fix: scroll event will be intercepted by tooltips

* fix: improve the logic to filter emojis or icons by keyword

* feat: add the recent icons and emojis to the selector

* refactor: optimize the code

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: LucasXu0 <lucas.xu@appflowy.io>

* fix: ensure the focus of emoji_search_bar not be lost within a second

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: LucasXu0 <lucas.xu@appflowy.io>
2024-12-26 09:55:29 +08:00
Lucas 07c4389f6a feat: refactor slash menu items to support showing in table cell (#7038)
* feat: refactor slash menu items to support showing in table cell

* chore: remove default slash menu items

* feat: support customizing slash commands in table cell

* fix: unable to delete math equation

* fix: underline issue

* feat: support text align in table

* fix: image alignment doesn't work in simple table cell

* fix: reduce the padding in table cell

* fix: drag column / row error in table

* test: support slash menu in table
2024-12-25 09:28:26 +08:00
Nathan.foooandMathias Mogensen 171c5634f2 fix: calculation UI refresh (#6615)
* chore: add test

* chore: update test

* chore: create events

* chore: load csv

* test: add events

* test: clean

* chore: fix test

* chore: fix test

* chore: fix test

* chore: fmt

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2024-12-24 21:27:34 +08:00
Lucas d7bda10e6f fix: do not wrap the inline math equation in a new line (#7035)
* fix: do not wrap the inline math equation in new line

* test: inline equation text and inline text align
2024-12-24 09:39:24 +08:00
Lucas 20bff9003e feat: support table align on desktop (#7034)
* feat: support table align on desktop

* test: update table align

* test: add integration test
2024-12-24 09:39:15 +08:00
Lucas 6e0534400b feat: reordering table columns and rows on mobile (#7036)
* feat: support table overflow

* fix: heading padding

* feat: support reordering table

* feat: highlight cell border when dragging

* chore: optimize the highlight border render logic

* fix: table menu error

* test: insert a table / insert column / insert row test

* test: delete column / delete row

* test: delete column / delete row

* test: enable header row / header column

* test: clear content, bold text
2024-12-23 21:07:09 +08:00
Lucas 17c116a53b feat: support table block on mobile (#7001)
* feat: support inserting table from + menu

* feat: support row/column action sheet

* feat: highlight reorder button

* feat: support table action mobile

* feat: support insert row / insert column

* feat: add more actions in the table bottom sheet

* feat: implement the action logic for table action menu

* feat: support clear content

* feat: enable header column / header row

* fix: unable to cancel highlight status after closing table action menu

* fix: table cell parse error

* feat: support insert left/insert right

* feat: add delete in table action menu

* feat: support copy a table row/column

* feat: support copy paste in table cell

* feat: support cut in table

* feat: disable cut if no content in clipboard

* chore: refactor table action bottom sheet

* feat: use ISimpleTableBottomSheetActions to manage the mobile action bottom sheet

* feat: add bold/text color/text background color in table bottom sheet

* feat: add align in table bottom sheet

* feat: support bold/text color/text background color update command

* feat: add align action to table action menu

* feat: integrate bold command in table bottom sheet

* feat: integrate text color and cell background color command in table bottom sheet

* feat: integrate text color command and cell background color command in table action menu

* chore: remove unused code

* fix: page style json parse error

* feat: support copy link to block / duplicate in table action menu

* feat: support align in table action menu

* feat: add quick actions in table action menu

* chore: update translations

* feat: integrate copy paste in table action menu

* feat: integrate align in table action menu

* fix: flutter ci

* test: add copy/paste/cut test

* fix: align the table with the document title

* fix: flutter tests

* fix: flutter ci
2024-12-23 10:29:41 +08:00
Richard Shiue 27b769362b chore: remove unused response state (#7031) 2024-12-23 10:03:20 +08:00
Richard Shiue b8e7d57ee6 fix: regenerating the last answer doesn't work (#7029)
* fix: regenerate not working for last message

* chore: code cleanup
2024-12-22 14:47:11 +08:00
Richard Shiue ed052c6792 chore: include parent view id in initial chat settings (#7030)
* chore: include parent view id in initial chat settings

* chjore: add a tooltip
2024-12-22 14:46:54 +08:00
Richard Shiue 5cd2c63320 fix: update rag_ids not saving to local store (#7028)
* fix: update rag_ids not saving to local store

* chore: bump up send button size
2024-12-21 14:31:48 +08:00
FakhriAzzouz 15567a81e9 chore(i18n): update ar-SA translations (#7017)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* 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-12-20 23:23:59 +08:00
Richard Shiue 7ab68dcc2c chore(flutter_desktop): ai chat ui improvements (#7025)
* chore: improve hover action bar inner radius

* chore: improve ai input appearance
2024-12-20 23:22:48 +08:00
Richard Shiue 30131fd9e4 chore: adjust select sources (#7019)
* chore: adjust select sources

* chore: restrict number of selected highest-level documents

* chore: ignore chat views

* chore: code cleanup
2024-12-20 22:43:26 +08:00
Richard Shiue ddcdd545d9 chore: regenerate response improvements (#7018)
* fix: disable sending message while streaming

* chore: don't allow regenerate while streaming
2024-12-20 11:42:43 +08:00
Nathan.fooo d25a399aba chore: sync chat document when open chat (#7016)
* chore: sync document

* chore: auto create chat title
2024-12-20 00:15:01 +08:00
ZackandLucas.Xu ee96a44fef feat: use new api to get the workspace member role (#6783)
* chore: update to latest client api

* chore: merge with main

* chore: remove unneeded code

* fix: sqlite migration

* fix: cargo fmt

* feat: use new api to get the member role

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-19 15:19:24 +08:00
Morn dda3962249 feat: combine icons and emojis into one menu(#6806) (#6998) 2024-12-19 15:19:10 +08:00
Morn c2743472bc fix: the index error when deleting text and then using the up or down arrow keys (#7009) 2024-12-19 14:40:11 +08:00
Richard Shiueandnathan e73fd56152 feat: regenerate ai response (#7006)
* feat: regenerate ai response

* chore: find question id instead of assuming

* chore: fix clippy

* chore: show local messages if they were there

* chore: remove duplicate code

* chore: fix loading message

* chore: revert unintended translation key removal

* chore: update translation for ai service unavailable

* chore: fix initial chat message load

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-12-19 14:13:53 +08:00
Richard Shiue 04a013f7ee chore: fix clippy (#7007) 2024-12-18 15:34:23 +08:00
Richard Shiue b966e3ea07 feat: select sources for ai prompt (#6975)
* feat: select sources for ai prompt

* feat: mobile

* chore: disable mention page

* chore: disable select sources when on local ai

* chore: code cleanup
2024-12-18 14:34:11 +08:00
Nathan.fooo 9d53f758d4 chore: bump client api 088 (#7004)
* chore: bump client api

* chore: bump client api

* chore: fix clippy
2024-12-18 12:12:58 +08:00
Lucas 0689f4e7e1 chore: bump version 0.7.8 (#7000)
* chore: bump version 0.7.8

* chore: use --locked to install duckscript
2024-12-17 17:59:27 +08:00
Lucas 867d515a35 feat: support readonly table (#6997)
* feat: support readonly table

* fix: cannot preview pasted images

* chore: remove http schema on android
2024-12-16 17:59:38 +08:00
Richard Shiue 381d946808 chore: filter out spaces from mention page (#6994) 2024-12-16 12:25:31 +08:00
Lucas f307300b96 fix: simple table issues (#6985)
* fix: list padding in table cell is too wide

* feat: improve tab in table cell

* feat: improve shift+tab in table cell

* fix: unable to edit cell after deleting an image

* fix: inline attribute issue

* fix: disable dragging a block into table

* feat: add distribute column evenly in column action menu

* fix: numbered list icon align in table cell

* feat: add setToPageWidth and distributeColumnEvenly in table menu

* feat: support highlight color

* chore: update editor version

* test: add setToPageWidth and distributeColumnEvenly in table menu

* test: inline attribute issues

* test: add distribute column evenly in column action menu

* test: select all in table

* test:  improve tab(+shift) shortcut in table cell

* test: improve enter shortcut in table cell

* feat: keep the same column width after using distribute column widths evenly

* test: keep the same column width after using distribute column widths evenly

* test: drag block to other block's child
2024-12-16 11:47:08 +08:00
Henri Devigne d74e7b63ca chore(i18n): update fr-FR translations 🐦 (#6987) 2024-12-16 11:10:14 +08:00
uxadax 792eec7d4b chore(i18n): update de-De translations (#6984)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-12-16 10:12:07 +08:00
Mathias Mogensen 67e93a12e6 fix: inline grid launch review (#6992) 2024-12-16 03:05:45 +01:00
Richard Shiue e188552c1e fix: ai chat initial rag_ids shouldn't include views that are not documents or in the trash (#6982)
* chore: only add document views to initial rag_ids

* chore: filter out views that are in the trash
2024-12-13 13:04:17 +08:00
Kilu.He ebb1b6dffb fix: simple table bugs (#6981) 2024-12-13 10:34:19 +08:00
Richard Shiue 25f9bee963 fix: launch review issues 0.7.8 (#6983)
* chore: mention popup height match design

* chore: adding while scrolling through mention page popup

* chore: don't add @ and restart creating mention when already in the middle of one

* fix: hide orphan views from mention page

* fix: arrow navigation wrap mention page popup breaks bloc
2024-12-13 10:20:56 +08:00
Lucas 6a6fac7f82 chore: bump version 0.7.8 (#6978) 2024-12-12 12:50:26 +08:00
Richard Shiue 555b4b48bb fix(flutter_desktop): cannot copy ai response with ctrl c (#6976) 2024-12-12 11:57:00 +08:00
Lucas 3522569f97 feat: support universal link / app link on mobile (#6973)
* feat: support universal link/app link

* feat: support app link on android
2024-12-12 10:03:21 +08:00
Morn 699ea150ce feat: able to select language for the code block by arrow keys (#6905) (#6964) 2024-12-12 09:27:24 +08:00
Mathias Mogensen e4385adfa9 fix: rename untitled view (#6789)
* fix: rename untitled view

* test: add cloud test

* chore: clean up code

* test: use nameOrDefault

* test: fix failing test

* test: fix wrong assumption
2024-12-12 02:21:23 +01:00
Mathias Mogensen 1d46923c47 feat: shrinkWrap grid in document (#6925)
* feat: shrinkWrap grid in document

* fix: clean up code and minor fixes

* test: add test w/ load more option

* fix: reinstate pageview & clean unused code

* fix: clean database tab bar view
2024-12-12 02:21:06 +01:00
Morn 0bf706f438 fix: Esc not working for Find-Replace menu(#6955) (#6965) 2024-12-12 08:18:57 +08:00
Richard Shiue 399b7dd682 test: attempt to fix flaky test (#6970) 2024-12-12 08:16:58 +08:00
Richard Shiue e8f2940024 fix(flutter): some ai chat bugs (#6969)
* chore: add hover effect and fix radius

* chore: open ref page on mobile
2024-12-11 21:48:38 +08:00
Lucas 62d5d66d20 feat: set table to page width (#6956)
* feat: set table to page width

* feat: expand the table based on the widht percentage

* test: set to page width

* feat: distribute columns evenly

* test: distribute columns evenly

* fix: border width
2024-12-11 16:37:28 +08:00
Lucas 8b672a159f feat: support multiple blocks operation (#6958)
* feat: support multiple blocks operation

* test: support multiple blocks operation
2024-12-11 16:37:07 +08:00
uxadax d68212f4ce chore(i18n): update de-DE translations 🐦 (#6938) 2024-12-11 15:30:52 +08:00
Lucas 592390dc84 feat: support multiple lines in table cell (#6931)
* feat: support multiple lines in table cell

* feat: add document validator

* fix: unable to delete a code block in table cell

* feat: drag to expand the table row

* fix: integration test

* feat: support drag to expand the table

* fix: integration test
2024-12-10 15:20:12 +08:00
Nathan.foooandLucas.Xu d2b2f17b1c chore: diagnose sync issues with sync.log file (#6950)
* chore: filter sync log

* chore: filter sync log

* chore: enable/disable sync log

* chore: enable/disable sync log

* chore: observer document and folder

* fix: integration test

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-10 10:02:48 +08:00
Richard Shiue 2c88653a69 feat(flutter_mobile): improve appearance of mention page selector (#6946)
* feat(flutter_mobile): improve appearance of mention page selector

* chore: move to use CustomScrollview
2024-12-09 17:20:39 +08:00
Lucas 45b4eb4b3a fix: hotfix issues for v0.7.7 (#6948)
* fix: include link preview block and file block in exported markdown

* test: include link preview block and file block in exported markdown

* chore: remove unused logs

* chore: update editor version

* fix: "+" menu should be close after pressing space

* test: cancel inline page reference menu by space

* chore: update editor version

* chore: remove unused logs
2024-12-09 16:19:37 +08:00
Richard Shiue 7b93bbe5ff fix(flutter_desktop): clicking on empty space when editing a cell sho… (#6949)
* fix(flutter_desktop): clicking on empty space when editing a cell shouldn't close event card

* test: fix integration tests
2024-12-09 15:13:37 +08:00
Ahad Patel d21c0c0dfc feat: add same delete design in database (#6620)
* add same delete design in database

* fix: remove padding when widget is null or function is null
2024-12-09 15:13:20 +08:00
Richard Shiue f5e46967ec fix(flutter): implement mention date transaction handler (#6933)
* fix: implement mention date transaction handler

* test: add integration tests

* chore: code cleanup

* chore: early return if null delta
2024-12-09 14:40:34 +08:00
Lucas e0885e2567 fix: image tests (#6928) 2024-12-09 09:17:07 +08:00
Richard Shiue 3b56887267 feat(flutter_desktop): AI chat refer to UI (#6930)
* chore: code cleanup

* feat(desktop): implement ai chat side panel

* chore: set min width for right side panel
2024-12-09 08:48:38 +08:00
Nathan.fooo b5d5312c70 chore: remove self-hosted runner (#6944) 2024-12-08 20:59:40 +08:00
Nathan.fooo 722b436cad chore: Ai chat context (#6929)
* chore: implement chat setting

* chore: clippy

* chore: rename

* chore: set rag_ids when creating a chat

* chore: clippy

* chore: fix test

* chore: fix test

* chore: fix test

* chore: clippy
2024-12-08 18:25:25 +08:00
Laura Bécognée bb50466aa9 chore(i18n): update fr-FR translations (#6936)
correct typo redémarer => redémarrer
2024-12-07 12:49:42 +08:00
Nathan.fooo da0395be5e chore: fetch rows by chunk size (#6934)
* chore: chunk size

* chore: chunk size
2024-12-07 00:02:56 +08:00
Lucas 9e82f3d7b8 fix: unable to open local file using afLaunchUrl function (#6927)
* fix: unable to open local file using afLaunchUrl function

* chore: use the latest api to open the local file

* chore: use the latest api to open the local file

* chore: use the latest api to open the local file

* test: add local paht regex test
2024-12-06 14:36:48 +08:00
Lucas 67fe0d6bfd feat: support column and row reordering in table (#6912)
* chore: update changelog

* feat: add draggable in table reorder button

* feat: support displaying text color, background color and font item in table cell

* feat: separate gestures for popup menu and drag operations

* feat: support feedback mode for table

* feat: build dummy node to render table feedback

* feat: disable column resize handle when dragging column

* feat: higtlight the cell border when dragging

* fix: unable to reorder in row

* fix: do not rebuild the reorder button when reordering

* feat: add reorder logic and tests

* feat: reorder column

* feat: reorder row

* test: reorder row

* fix: table attributes are broken after reordering

* chore: remove unused listerner

* chore: code refactor

* fix: remove unused code

* feat: support rendering table feedback

* fix: unit test
2024-12-06 09:22:32 +08:00
Nathan.fooo dddf5aa195 chore: move type option to collab repo (#6921)
* chore: bump collab

* chore: bump collab

* chore: fix test compile

* chore: fix test

* chore: remove numeric

* chore: fix media type option
2024-12-05 14:12:25 +08:00
Richard Shiue 92945cafdf fix: initial ai chat load (#6920) 2024-12-04 20:53:03 +08:00
Kilu.He 5cf6617231 fix: simple table width (#6918)
* fix: adjust min width

* fix: adjust simple table font size

* fix: do not need to run rust-ci and docker-ci when web codes have been changed
2024-12-04 16:16:48 +08:00
Richard Shiue 03c84ff8b5 feat: open ai response url source in browser (#6917) 2024-12-03 22:55:34 +08:00
Richard Shiue 0cf3ade332 fix(desktop): resize sidebar menu regression (#6897) 2024-12-03 22:20:30 +08:00
Richard Shiue 7c24b6feb0 feat: revamp mention page interactions in AI chat (#6896)
* chore: code cleanup

* chore: improve mention page ui

* chore: just use view pb

* chore: remove chat input menu style

* chore: code cleanup

* chore: rewrite and unify chat input action handler and bloc

* feat: improve appearance of mention page popup

* fix: misaligned emoji text
2024-12-03 22:20:14 +08:00
Lucas 687121ff14 chore: upgrade rust version to 1.80.1 (#6916) 2024-12-03 21:38:28 +08:00
Lucas e32c584b3b fix: remove extra padding in table on mobile (#6915) 2024-12-03 20:41:52 +08:00
Lucas 64e4416f54 chore: release version 0.7.6 (#6910)
* chore: update changelog

* feat: support displaying text color, background color and font item in table cell

* fix: integration test and unit test
2024-12-03 17:27:59 +08:00
Nathan.fooo ca195887e0 chore: bump collab (#6914)
* chore: bump collab

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

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

* chore: upgrade appflowy_editor

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

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

* fix: focus on the first cell after inserting table

* test: focus on the first cell after inserting table

* feat: highlight the cell when editing

* test: highlight the cell when editing

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

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

* chore: rename simple table components

* fix: select all in table cell block

* test: select all in table cell block

* feat: disable two-fingers resize in table cell

* feat: includ table when exporting markdown

* test: include table when exporting markdown

* feat: optimize add row button render logic

* chore: optimize hover button render logic

* fix: column button is not clickable

* fix: theme assertion

* feat: improve hovering logic

* fix: selection issue in table

* fix(flutter_desktop): popover conflicts on simple table

* feat: support table markdown import

* test: table cell isEditing test

* test: select all in table test

* fix: popover conflict in table action menu

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

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

* test: enable header column and header row in table

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

* chore: duplicate table in optin menu

* fix: integraion test

---------

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

* test: add tests for pin/unpin feature

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

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

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

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

* fix: view editor used during initialization

* fix: quick and dirty hack job

* test: add test

* chore: don't create separate mut var

* chore: docs

* fix: uninitialized group controllers

* chore: remove group

* chore: fix test

---------

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

* fix: close keyboard when uploading file

* feat: integrate a simple table

* feat: support selectable mixin for table

* feat: render border for table

* feat: support add column/row

* feat: support add column and row

* feat: support add column and row button

* feat: support table border and cell border

* feat: use context to manage hovering status

* feat: add hover status for cell

* feat: add hover row action

* feat: support row/column hover action

* feat: support table more actions

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

* feat: add reorder button

* feat: clear content in table

* feat: support header column and header row

* chore: refactor the table more actions

* feat: render border in cell

* feat: add align button

* feat: add align menu

* fix: unable to delete last column

* feat: support drag to resize

* feat: support resizing column and set limitation

* fix: unable to resize

* feat: support table cell align

* fix: type error

* feat: support table cell background color

* fix: enable header column/row bug

* feat: support table background color

* feat: support duplicate table without background color

* feat: support duplicate with column color

* feat: update column width

* feat: move insert operations to a new file

* fix: row more action position is not correct

* fix: delete row error

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

* feat: support mapping the insertion color/align

* feat: support mapping the duplication color/align

* fix: delete column error

* feat: support duplication map

* feat: support duplicating column with color and align

* feat: support duplicating row with color and align

* chore: optimize the table_map_operations

* feat: support clear content / header / style operations

* chore: remove null value in table attributes

* fix: delete row issues

* fix: delete column issues

* fix: unable to clear content

* feat: support arrow key shortcuts

* feat: support tab shortcuts

* feat: support backspace key shortcuts

* feat: support table block selection

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

* fix: flutter analyze

* chore: enable debug log in table

* test: simple table insert operation

* test: simple table delete operation

* test: simple table header operation

* test: simple table style operation - column width

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

* test: simple table content operation - clear content

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

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

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

* test: add integration test and i18n

* chore: update translations

* fix: drag visual issue

* fix: exclude the popup menu in table more actions

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

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

* fix: use shrinkWrap to decide height

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

* feat: support nested list

* test: add markdown text robot tests

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

* feat: support markdown in ask ai block

* feat: render ai result with appflowy_editor

* feat: support markdown syntax in ask ai block

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

* feat: optimize text insertion

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

* chore: related question alignment

* chore: adjust vertical spacing around sources

* chore: scroll to bottom animation improvement

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

* fix: flutter analyze

* feat: add sentence mode

* test: support skipping in-memory update transaction

* test: add sentence mode

* test: add sentence mode (2)

* chore: set enableDocumentInternalLog to false

* fix: integration test

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

* fix: missing context in drop zone

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

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

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

* fix: toggle list rtl issue

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

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

* chore: remove unrelated message

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

* test: update

* chore: bump client api

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

* test: close other tabs before switching workspace

* chore: update release note

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

* feat: hide the row page from sidebar

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

* fix: update drag block logic

* feat: support toggle heading in outline

* test: add toggle headings show in outline block test

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

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

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

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

* chore: format code

* chore: update publish toast

* feat: optimize the toast for mobile more actions menu

* feat: optimize update path name logic

* test: add update path name test

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

* fix: messages arriving out of order

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

* fix: contrast issue for the delte dialog text

* fix: stay in space menu after deleting a space

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

* feat: expand the hit test area for toggle button

* feat: remove open workspace success toast

* chore: update translations

* test: stay in space menu after deleting a space

* chore: enable android test

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

* fix: double menu showing in workspace menu

* feat: only display copy in ai message

* test: add test

* test: fix integration test

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

* chore: redundant check for cloud and use guard clause

* feat: improve ai prompt input ui

* feat: improve ai chat welcome page

* feat: improve loading status indicator

* feat: improve chat bubble avatars

* chore: clean up chat theme

* feat: improve sizing, padding and text styles

* feat: improve sugested follow up question style

* feat: improve error message style

* feat: improve mobile chat ui

* feat: add superman

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

* feat: ai response action bottom sheet

* chore: code cleanup

* feat: improve look of side panel

* chore: BIG ANNOUNCEMENT CITY

* fix: test session issues

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

* fix: empty query not resetting search service

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

* chore: remove unused focus node and text controller

* chore: bump appflowy editor

* chore: code cleanup

* chore: fix focus getting lost on submission

* fix: next match focuses on title after jumping

* chore: bump appflowy editor

* revert: unnecessary changes to FlowyFormTextInput

* fix: title requesting focus unexpectedly

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

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

* chore: merge conflicts

* chore: code cleanup

* test: add integration test

* fix: show replace menu icon color in dark mode

---------

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

* chore: update appflowy_editor

* test: tab menu test

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

* feat: add toggle headings in plus menu

* test: add toggle heading block test

* test: toogle heading 1 block test

* test: add toggle heading selection test

* fix: toggle headings test

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

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

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

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

* feat: add unpublished information

* feat: keep the last publish name

* test: add re-publish test

* test: add empty name test

* chore: update cloud version to 0.7.6

---------

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

* chore: add workspace menu animation

* feat: optimize workspace menu loading speed on mobile

* test: workspace member count test

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

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

* feat: support editing a space on mobile

* feat: support renaming space

* feat: add more space icons

* fix: unable to rename space

* fix: code review

* chore: remove ingore import flag for testing

* test: add create space test

* chore: enable cloud test

* test: fix space test

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

* feat: integrate codemagic

* Revert "fix: pages overflow when selecting homepage"

This reverts commit 156882a9a7.

* chore: try to fix build

* chore: remove flutter analyuze

* chore: launch ios simulator

* fix: flutter version

* fix: integration tests on mobile

* fix: mobile tests

* test: fix page style test

* chore: enable ios ci

* chore: update codemagic token

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

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

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

* fix: attempt to fix date reminder

* chore: alignment issue on windows

* chore: use onFailure

* chore: default for set reminder

* chore: code style

* fix: add reminder bug

* fix: checklist item alignment

* fix: date cell reminders

* chore: regard cell datetime as correct

* fix: reminder creation date

* chore: bump collab that fixes reminders

* test: add date cell and reminder bloc test

* chore: bump collab

* chore: revert visual density change

* fix: submitting date time text field makes it flash

* fix: improve behavior of phantom checklist item

---------

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

* chore: update win32_window

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

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

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

* fix: children disapper when using toggle shortcuts

* fix: add tooltip for namespace button

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

* fix: rename insert page item
2024-11-06 00:50:27 +01:00
3489 changed files with 53669 additions and 149210 deletions

No files matched your search

+178 -108
View File
@@ -1,126 +1,196 @@
# 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/**"
# 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.81.0"
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 --locked 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
+2 -2
View File
@@ -2,9 +2,9 @@ name: Docker-CI
on:
push:
branches: ["main", "release/*"]
branches: [ "main", "release/*" ]
pull_request:
branches: ["main", "release/*"]
branches: [ "main", "release/*" ]
workflow_dispatch:
concurrency:
+11 -11
View File
@@ -26,7 +26,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.2"
RUST_TOOLCHAIN: "1.80.1"
RUST_TOOLCHAIN: "1.81.0"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64
@@ -40,7 +40,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -74,7 +74,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [windows-latest]
os: [ windows-latest ]
include:
- os: windows-latest
flutter_profile: development-windows-x86
@@ -101,7 +101,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [macos-latest]
os: [ macos-latest ]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
@@ -123,12 +123,12 @@ jobs:
flutter_profile: ${{ matrix.flutter_profile }}
unit_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -217,11 +217,11 @@ jobs:
shell: bash
cloud_integration_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -340,13 +340,13 @@ jobs:
shell: bash
integration_test:
needs: [prepare-linux]
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]
os: [ ubuntu-latest ]
test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
+24 -22
View File
@@ -7,7 +7,6 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
pull_request:
@@ -16,13 +15,11 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
env:
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.80.1"
CLOUD_VERSION: 0.6.51
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.81.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -86,7 +83,7 @@ jobs:
working-directory: frontend
run: |
rustup target install aarch64-apple-ios-sim
cargo install --force duckscript_cli
cargo install --force --locked duckscript_cli
cargo install cargo-lipo
cargo make appflowy-flutter-deps-tools
shell: bash
@@ -97,21 +94,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 }}
+20 -20
View File
@@ -7,7 +7,7 @@ on:
env:
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.77.2"
RUST_TOOLCHAIN: "1.81.0"
jobs:
create-release:
@@ -73,8 +73,8 @@ jobs:
working-directory: frontend
run: |
vcpkg integrate install
cargo install --force cargo-make
cargo install --force duckscript_cli
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
- name: Build Windows app
working-directory: frontend
@@ -135,7 +135,7 @@ jobs:
fail-fast: false
matrix:
job:
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
- { target: x86_64-apple-darwin, os: macos-13, extra-build-args: "" }
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -158,8 +158,8 @@ jobs:
- name: Install prerequisites
working-directory: frontend
run: |
cargo install --force cargo-make
cargo install --force duckscript_cli
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
- name: Build AppFlowy
working-directory: frontend
@@ -232,10 +232,10 @@ jobs:
matrix:
job:
- {
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-latest,
extra-build-args: "",
}
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-latest,
extra-build-args: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -256,8 +256,8 @@ jobs:
- name: Install prerequisites
working-directory: frontend
run: |
cargo install --force cargo-make
cargo install --force duckscript_cli
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
- name: Build AppFlowy
working-directory: frontend
@@ -336,12 +336,12 @@ jobs:
matrix:
job:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -370,8 +370,8 @@ jobs:
sudo apt-get install keybinder-3.0
sudo apt-get install -y alien libnotify-dev
source $HOME/.cargo/env
cargo install --force cargo-make
cargo install --force duckscript_cli
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
rustup target add ${{ matrix.job.target }}
- name: Install gcc-aarch64-linux-gnu
+4 -75
View File
@@ -15,85 +15,14 @@ on:
- "main"
- "develop"
- "release/*"
paths:
- "frontend/rust-lib/**"
env:
CARGO_TERM_COLOR: always
CLOUD_VERSION: 0.6.51-amd64
RUST_TOOLCHAIN: "1.77.2"
CLOUD_VERSION: 0.8.3-amd64
RUST_TOOLCHAIN: "1.81.0"
jobs:
self-hosted-job:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Checkout Appflowy Cloud
uses: actions/checkout@v4
with:
repository: AppFlowy-IO/AppFlowy-Cloud
path: AppFlowy-Cloud
- name: Prepare Appflowy Cloud env
working-directory: AppFlowy-Cloud
run: |
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
- name: Ensure AppFlowy-Cloud is Running with Correct Version
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. Pulling the correct version..."
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
- name: Run rust-lib tests
working-directory: frontend/rust-lib
env:
RUST_LOG: info
RUST_BACKTRACE: 1
af_cloud_test_base_url: http://localhost
af_cloud_test_ws_url: ws://localhost/ws/v1
af_cloud_test_gotrue_url: http://localhost/gotrue
run: |
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
- name: rustfmt rust-lib
run: cargo fmt --all -- --check
working-directory: frontend/rust-lib/
- name: clippy rust-lib
run: cargo clippy --all-targets -- -D warnings
working-directory: frontend/rust-lib
ubuntu-job:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Set timezone for action
@@ -137,7 +66,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
@@ -160,7 +89,7 @@ jobs:
else
echo "No volumes to remove."
fi
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
+3 -3
View File
@@ -11,7 +11,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.77.2"
RUST_TOOLCHAIN: "1.81.0"
jobs:
tests:
@@ -40,8 +40,8 @@ jobs:
- name: Install prerequisites
working-directory: frontend
run: |
cargo install --force cargo-make
cargo install --force duckscript_cli
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
- uses: Swatinem/rust-cache@v2
with:
-124
View File
@@ -1,124 +0,0 @@
name: Tauri-CI
on:
pull_request:
paths:
- ".github/workflows/tauri2_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.77.2"
CARGO_MAKE_VERSION: "0.36.6"
CI: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# tauri-build-self-hosted:
# if: github.event.pull_request.head.repo.full_name == github.repository
# runs-on: self-hosted
#
# steps:
# - uses: actions/checkout@v4
# - name: install frontend dependencies
# working-directory: frontend/appflowy_web_app
# run: |
# mkdir dist
# pnpm install
# cd src-tauri && cargo build
#
# - name: test and lint
# working-directory: frontend/appflowy_web_app
# run: |
# pnpm run lint:tauri
#
# - uses: tauri-apps/tauri-action@v0
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# tauriScript: pnpm tauri
# projectPath: frontend/appflowy_web_app
# args: "--debug"
tauri-build-ubuntu:
#if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Maximize build space
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
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install dependencies
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: install tauri deps tools
working-directory: frontend
run: |
cargo make appflowy-tauri-deps-tools
shell: bash
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
mkdir dist
pnpm install
cd src-tauri && cargo build
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint:tauri
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_web_app
args: "--debug"
-111
View File
@@ -1,111 +0,0 @@
name: Tauri-CI
on:
push:
branches:
- build/tauri
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.77.2"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tauri-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
env:
CI: true
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
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
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_tauri/node_modules
key: node-modules-${{ runner.os }}
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
working-directory: frontend
run: |
cargo install --force duckscript_cli
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install cargo-make
working-directory: frontend
run: |
cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools
- name: install frontend dependencies
working-directory: frontend/appflowy_tauri
run: |
mkdir dist
pnpm install
cargo make --cwd .. tauri_build
- name: frontend tests and linting
working-directory: frontend/appflowy_tauri
run: |
pnpm test
pnpm test:errors
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_tauri
args: "--debug"
-153
View File
@@ -1,153 +0,0 @@
name: Publish Tauri Release
on:
workflow_dispatch:
inputs:
branch:
description: 'The branch to release'
required: true
default: 'main'
version:
description: 'The version to release'
required: true
default: '0.0.0'
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.77.2"
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
settings:
- platform: windows-latest
args: "--verbose"
target: "windows-x86_64"
- platform: macos-latest
args: "--target x86_64-apple-darwin"
target: "macos-x86_64"
- platform: ubuntu-20.04
args: "--target x86_64-unknown-linux-gnu"
target: "linux-x86_64"
runs-on: ${{ matrix.settings.platform }}
env:
CI: true
PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}
- name: Maximize build space (ubuntu only)
if: matrix.settings.platform == 'ubuntu-20.04'
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
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
- name: install dependencies (windows only)
if: matrix.settings.platform == 'windows-latest'
working-directory: frontend
run: |
cargo install --force duckscript_cli
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install cargo-make
working-directory: frontend
run: |
cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools
- name: install frontend dependencies
working-directory: frontend/appflowy_tauri
run: |
mkdir dist
pnpm install
pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }}
cargo make --cwd .. tauri_build
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }}
APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }}
APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }}
CI: true
with:
args: ${{ matrix.settings.args }}
appVersion: ${{ github.event.inputs.version }}
tauriScript: pnpm tauri
projectPath: frontend/appflowy_tauri
- name: Upload EXE package(windows only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'windows-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.exe
path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe
- name: Upload DMG package(macos only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'macos-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.dmg
path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg
- name: Upload Deb package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.deb
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
- name: Upload AppImage package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.AppImage
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
-75
View File
@@ -1,75 +0,0 @@
name: Web-CI
on:
pull_request:
paths:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
web-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
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
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
pnpm install
- name: Run lint check
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
- name: build and analyze
working-directory: frontend/appflowy_web_app
run: |
pnpm run analyze >> analyze-size.txt
- name: Upload analyze-size.txt
uses: actions/upload-artifact@v4
with:
name: analyze-size.txt
path: frontend/appflowy_web_app/analyze-size.txt
retention-days: 30
- name: Upload stats.html
uses: actions/upload-artifact@v4
with:
name: stats.html
path: frontend/appflowy_web_app/dist/stats.html
retention-days: 30
-65
View File
@@ -1,65 +0,0 @@
name: Web Code Coverage
on:
pull_request:
paths:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
if: github.event.pull_request.draft != true
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
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
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
# Install pnpm dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v6
with:
working-directory: frontend/appflowy_web_app
component: true
build: pnpm run build
start: pnpm run start
browser: chrome
- name: Jest run
working-directory: frontend/appflowy_web_app
run: |
pnpm run test:unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
token: cf9245e0-e136-4e21-b0ee-35755fa0c493
files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info
flags: appflowy_web_app
name: frontend/appflowy_web_app
fail_ci_if_error: true
verbose: true
+122 -1
View File
@@ -1,4 +1,125 @@
# Release Notes
## Version 0.8.1 - 14/01/2025
### New Features:
- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only
- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat
- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language
- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more
- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar
- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile
### Bug Fixes
- Resolved an icon rendering issue in callout blocks, tab bars, and search results
- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails
## Version 0.8.0 - 06/01/2025
### Bug Fixes
- Fixed error displaying in the page style menu
- Fixed filter logic in the icon picker
- Fixed error displaying in the Favorite/Recent page
- Fixed the color picker displaying when tapping down
- Fixed icons not being supported in subpage blocks
- Fixed recent icon functionality in the space icon menu
- Fixed "Insert Below" not auto-scrolling the table
- Fixed a to-do item with an emoji automatically creating a soft break
- Fixed header row/column tap areas being too small
- Fixed simple table alignment not working for items that wrap
- Fixed web content reverting after removing the inline code format on desktop
- Fixed inability to make changes to a row or column in the table when opening a new tab
- Fixed changing the language to CKB-KU causing a gray screen on mobile
## Version 0.7.9 - 30/12/2024
### New Features
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
- Create beautiful documents with 22 content types and markdown support
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
- Invite members to your workspace for seamless collaboration
- Create multiple public/private spaces to better organize your content
- Simple Table is now available on Mobile, designed specifically for mobile devices.
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
- Use '/' to insert a content block into a table cell on Desktop
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
- Add messages to an editable document while chatting with AI side by side
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
- Drag a page from the sidebar into a document to easily mention the page without typing its title
- Paste as plain text, a new option in the right-click paste menu
### Bug Fixes
- Fixed misalignment in numbered lists
- Resolved several bugs in the emoji menu
- Fixed a bug with checklist items
## Version 0.7.8 - 18/12/2024
### New Features
<img width="1068" alt="image" src="https://github.com/user-attachments/assets/cf8bd287-f370-4291-8638-76e2bbf4aaac" />
- Meet Simple Table 2.0:
- Insert a list into a table cell
- Insert images, quotes, callouts, and code blocks into a table cell
- Drag to move rows or columns
- Toggle header rows or columns on/off
- Distribute columns evenly
- Adjust to page width
- Enjoy a new UI/UX for a seamless experience
- Revamped mention page interactions in AI Chat
- Improved AppFlowy AI service
### Bug Fixes
- Fixed an error when opening files in the database in local mode
- Fixed arrow up/down navigation not working for selecting a language in Code Block
- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work
## Version 0.7.7 - 09/12/2024
### Bug Fixes
- Fixed sidebar menu resize regression
- Fixed AI chat loading issues
- Fixed inability to open local files in database
- Fixed mentions remaining in notifications after removal from document
- Fixed event card closing when clicking on empty space
- Fixed keyboard shortcut issues
## Version 0.7.6 - 03/12/2024
### New Features
- Revamped the simple table UI
- Added support for capturing images from camera on mobile
### Bug Fixes
- Improved markdown rendering capabilities in AI writer
- Fixed an issue where pressing Enter on a collapsed toggle list would add an unnecessary new line
- Fixed an issue where creating a document from slash menu could insert content at incorrect position
## Version 0.7.5 - 25/11/2024
### Bug Fixes
- Improved chat response parsing
- Fixed toggle list icon direction for RTL mode
- Fixed cross blocks formatting not reflecting in float toolbar
- Fixed unable to click inside the toggle list to create a new paragraph
- Fixed open file error 50 on macOS
- Fixed upload file exceed limit error
## Version 0.7.4 - 19/11/2024
### New Features
- Support uploading WebP and BMP images
- Support managing workspaces on mobile
- Support adding toggle headings on mobile
- Improve the AI chat page UI
### Bug Fixes
- Optimized the workspace menu loading performance
- Optimized tab switching performance
- Fixed searching issues in Document page
## Version 0.7.3 - 07/11/2024
### New Features
- Enable custom URLs for published pages
- Support toggling headings
- Create a subpage by typing in the document
- Turn selected blocks into a subpage
- Add a manual date picker for the Date property
### Bug Fixes
- Fixed an issue where the workspace owner was unable to delete spaces created by others
- Fixed cursor height inconsistencies with text height
- Fixed editing issues in Kanban cards
- Fixed an issue preventing images or files from being dropped into empty paragraphs
## Version 0.7.2 - 22/10/2024
### New Features
- Copy link to block
@@ -905,4 +1026,4 @@ Bug fixes and improvements
- Increased height of action
- CPU performance issue
- Fix potential data parser error
- More foundation work for online collaboration
- More foundation work for online collaboration
+48 -17
View File
@@ -42,10 +42,12 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
## User Installation
- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases)
- Other channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
- Other
channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
- Available on
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is not supported
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is
not supported
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
@@ -61,15 +63,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
## Getting Started with development
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific
development instructions
## Roadmap
- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
If you'd like to propose a feature, submit a feature
request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit a bug
report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
## **Releases**
@@ -77,16 +82,24 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
## Contributing
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details.
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make
are **greatly appreciated**. Please look
at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy)
for details.
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly
easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains
the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with
us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
## Translations 🌎🗺
[![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use
the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or
run `npx inlang machine translate` to add missing translations.
## Join the community to build AppFlowy together
@@ -96,16 +109,30 @@ To add translations, you can manually edit the JSON translation files in `/front
## Why Are We Building This?
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and
functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations.
These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative
workplace management tools also have their constraints.
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market.
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have
led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates
from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a
proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to
come up with a one-size fits all solution in such a fragmented market.
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well.
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up,
in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is
a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention
the speed and native experience. The same may apply to individual users as well.
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs
well.
- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to
enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy
your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term
maintainability.
We decided to achieve this mission by upholding the three most fundamental values:
@@ -113,16 +140,20 @@ We decided to achieve this mission by upholding the three most fundamental value
- Reliable native experience
- Community-driven extensibility
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority
doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the
knowledge and wheels of making complex workplace management tools while enabling people and businesses to create
beautiful things on their own by equipping them with a versatile toolbox of building blocks.
## License
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information.
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for
more information.
## Acknowledgments
Special thanks to these amazing projects which help power AppFlowy.IO:
- [flutter-quill](https://github.com/singerdmx/flutter-quill)
- [cargo-make](https://github.com/sagiegurari/cargo-make)
- [contrib.rocks](https://contrib.rocks)
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
+47
View File
@@ -0,0 +1,47 @@
workflows:
ios-workflow:
name: iOS Workflow
instance_type: mac_mini_m2
max_build_duration: 30
environment:
flutter: 3.22.3
xcode: latest
cocoapods: default
scripts:
- name: Build Flutter
script: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
rustc --version
cargo --version
cd frontend
rustup target install aarch64-apple-ios-sim
cargo install --force cargo-make
cargo install --force --locked 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
-15
View File
@@ -121,20 +121,5 @@
// "request": "launch",
// "program": "[YOUR_APPLICATION_PATH]",
},
{
// https://tauri.app/v1/guides/debugging/vs-code
"type": "lldb",
"request": "launch",
"name": "AF-tauri: Debug backend",
"cargo": {
"args": [
"build",
"--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml",
"--no-default-features"
]
},
"preLaunchTask": "AF: Tauri UI Dev",
"cwd": "${workspaceRoot}/appflowy_tauri/"
},
]
}
-45
View File
@@ -245,51 +245,6 @@
"problemMatcher": [],
"detail": "appflowy_flutter"
},
{
"label": "AF: Tauri UI Build",
"type": "shell",
"command": "pnpm run build",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Tauri UI Dev",
"type": "shell",
"isBackground": true,
"command": "pnpm sync:i18n && pnpm run dev",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Tauri Clean",
"type": "shell",
"command": "cargo make tauri_clean",
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "AF: Tauri Clean + Dev",
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Tauri Clean",
"AF: Tauri UI Dev"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "AF: Tauri ESLint",
"type": "shell",
"command": "npx eslint --fix src",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Generate Env File",
"type": "shell",
+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.3"
APPFLOWY_VERSION = "0.8.1"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -53,7 +53,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
minSdkVersion 29
targetSdkVersion 34
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -36,7 +36,6 @@
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="appflowy-flutter" />
<!-- <data android:host="login-callback" /> -->
</intent-filter>
</activity>
<!--
@@ -50,7 +49,8 @@
<!-- Permission to read files from external storage (outside application container).
As of Android 12 this permission no longer has any effect. Instead use the
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READM_MEDIA_AUDIO permissions. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Permissions to read media files. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<queries>
@@ -65,4 +65,5 @@
-->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.CAMERA" />
</manifest>
@@ -1,16 +1,20 @@
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'document/document_test_runner.dart' as document_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'sidebar/sidebar_rename_untitled_test.dart'
as sidebar_rename_untitled_test;
import 'uncategorized/uncategorized_test_runner.dart'
as uncategorized_test_runner;
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
Future<void> main() async {
preset_af_cloud_env_test.main();
data_migration_test_runner.main();
// uncategorized
uncategorized_test_runner.main();
@@ -22,4 +26,6 @@ Future<void> main() async {
// sidebar
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
}
@@ -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,47 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('AI Writer:', () {
testWidgets('the ai writer transaction should only apply in memory',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_aiWriter.tr(),
);
expect(find.byType(AIWriterBlockComponent), findsOneWidget);
// switch to another page
await tester.openPage(Constants.gettingStartedPageName);
// switch back to the page
await tester.openPage(pageName);
// expect the ai writer block is not in the document
expect(find.byType(AIWriterBlockComponent), findsNothing);
});
});
}
@@ -1,33 +1,17 @@
// ignore_for_file: unused_import
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/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_tab.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/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/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -122,9 +106,21 @@ void main() {
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()));
@@ -153,5 +149,72 @@ void main() {
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,
);
});
});
}
@@ -1,5 +1,6 @@
import 'package:integration_test/integration_test.dart';
import 'document_ai_writer_test.dart' as document_ai_writer_test;
import 'document_copy_link_to_block_test.dart'
as document_copy_link_to_block_test;
import 'document_option_actions_test.dart' as document_option_actions_test;
@@ -11,4 +12,5 @@ void main() {
document_option_actions_test.main();
document_copy_link_to_block_test.main();
document_publish_test.main();
document_ai_writer_test.main();
}
@@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/emoji.dart';
import '../../../shared/util.dart';
void main() {
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
testWidgets('Change slide bar space icon', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
final emojiIconData = await tester.loadIcon();
final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
await tester.hoverOnWidget(
find.byType(SidebarSpaceHeader),
onHover: () async {
final moreOption = find.byType(SpaceMorePopup);
await tester.tapButton(moreOption);
expect(find.byType(FlowyIconEmojiPicker), findsNothing);
await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg);
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
},
);
final icons = find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
);
expect(icons, findsOneWidget);
await tester.tapIcon(EmojiIconData.icon(firstIcon));
final spaceHeader = find.byType(SidebarSpaceHeader);
final spaceIcon = find.descendant(
of: spaceHeader,
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
),
);
expect(spaceIcon, findsOneWidget);
});
}
@@ -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() {
@@ -0,0 +1,55 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_input.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();
testWidgets('Rename empty name view (untitled)', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
);
// click the ... button and open rename dialog
await tester.hoverOnPageName(
ViewLayoutPB.Document.defaultName,
onHover: () async {
await tester.tapPageOptionButton();
await tester.tapButtonWithName(
LocaleKeys.disclosureAction_rename.tr(),
);
},
);
await tester.pumpAndSettle();
expect(find.byType(NavigatorTextFieldDialog), findsOneWidget);
final textField = tester.widget<FlowyFormTextInput>(
find.descendant(
of: find.byType(NavigatorTextFieldDialog),
matching: find.byType(FlowyFormTextInput),
),
);
expect(
textField.controller!.text,
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
});
}
@@ -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,20 @@
// 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/shared/loading.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/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/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 +75,138 @@ 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('workspace menu popover behavior test', (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,
);
// the workspace menu shouldn't conflict with logout
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);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
final logoutButton = find.byType(WorkspaceMoreButton);
await tester.tapButton(logoutButton);
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
expect(moreButton, findsNothing);
await tester.tapButton(moreButton);
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
expect(moreButton, findsOneWidget);
},
);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// clicking on the more action button for the same workspace shouldn't do
// anything
await tester.openCollaborativeWorkspaceMenu();
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);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
// click it again
await tester.tapButton(moreButton);
// nothing should happen
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
},
);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// clicking on the more button of another workspace should close the menu
// for this one
await tester.openCollaborativeWorkspaceMenu();
final moreButton = find.byWidgetPredicate(
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
);
await tester.hoverOnWidget(
workspaceItem,
onHover: () async {
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
},
);
final otherWorspaceItem = find.byWidgetPredicate(
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
);
final otherMoreButton = find.byWidgetPredicate(
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
);
await tester.hoverOnWidget(
otherWorspaceItem,
onHover: () async {
expect(otherMoreButton, findsOneWidget);
await tester.tapButton(otherMoreButton);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
expect(moreButton, findsNothing);
},
);
});
});
}
@@ -1,29 +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();
@@ -66,7 +54,7 @@ void main() {
);
final shareValues = plainText!
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, isNotEmpty);
@@ -0,0 +1,87 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/shared/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);
});
testWidgets('the space view should not be opened', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
expect(find.byType(AppFlowyEditorPage), findsNothing);
expect(find.text('Blank page'), findsOne);
});
});
}
@@ -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,46 +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/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/shared/share/publish_tab.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_item.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/home_page_menu.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/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() {
@@ -272,7 +249,7 @@ More actions for published page:
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
await tester.wait(2000);
// check if the page is published in sites page
final pageItem = find.byWidgetPredicate(
@@ -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();
}
@@ -1,6 +1,15 @@
import 'dart:convert';
import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.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_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -8,7 +17,9 @@ import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
});
group('Folder Search', () {
testWidgets('Search for views', (tester) async {
@@ -45,9 +56,94 @@ void main() {
await tester.pumpAndSettle(const Duration(seconds: 1));
// The score should be higher for "ViewOne" thus it should be shown first
final firstDocumentWidget = tester
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
final firstDocumentWidget = tester.widget(
find.byType(SearchResultTile).first,
) as SearchResultTile;
expect(firstDocumentWidget.result.data, firstDocument);
});
testWidgets('Displaying icons in search results', (tester) async {
final randomValue = Random().nextInt(10000) + 10000;
final pageNames = ['First Page-$randomValue', 'Second Page-$randomValue'];
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final emojiIconData = await tester.loadIcon();
/// create two pages
for (final pageName in pageNames) {
await tester.createNewPageWithNameUnderParent(name: pageName);
await tester.updatePageIconInTitleBarByName(
name: pageName,
layout: ViewLayoutPB.Document,
icon: emojiIconData,
);
}
await tester.toggleCommandPalette();
/// search for `Page`
final searchFieldFinder = find.descendant(
of: find.byType(SearchField),
matching: find.byType(FlowyTextField),
);
await tester.enterText(searchFieldFinder, 'Page-$randomValue');
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.byType(SearchResultTile), findsNWidgets(2));
/// check results
final svgs = find.descendant(
of: find.byType(SearchResultTile),
matching: find.byType(FlowySvg),
);
expect(svgs, findsNWidgets(2));
final firstSvg = svgs.first.evaluate().first.widget as FlowySvg,
lastSvg = svgs.last.evaluate().first.widget as FlowySvg;
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
/// icon displayed correctly
expect(firstSvg.svgString, iconData.svgString);
expect(lastSvg.svgString, iconData.svgString);
testWidgets('select the content in document and search', (tester) async {
const firstDocument = ''; // empty document
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: firstDocument);
await tester.editor.updateSelection(
Selection(
start: Position(
path: [0],
),
end: Position(
path: [0],
offset: 10,
),
),
);
await tester.pumpAndSettle();
expect(
find.byType(FloatingToolbar),
findsOneWidget,
);
await tester.toggleCommandPalette();
expect(find.byType(CommandPaletteModal), findsOneWidget);
expect(
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
findsOneWidget,
);
expect(
find.text(firstDocument),
findsOneWidget,
);
});
});
});
}
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('calendar', () {
testWidgets('update calendar layout', (tester) async {
@@ -301,6 +309,7 @@ void main() {
await tester.createOption(name: "qwer");
await tester.selectOption(name: "asdf");
await tester.dismissCellEditor();
await tester.dismissCellEditor();
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, "Tags");
@@ -332,6 +341,7 @@ void main() {
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
await tester.selectOption(name: "asdf");
await tester.dismissCellEditor();
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(0);
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('grid edit field test:', () {
testWidgets('rename existing field', (tester) async {
@@ -538,8 +545,8 @@ void main() {
// edit the first date cell
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.toggleIncludeTime();
final now = DateTime.now();
await tester.toggleIncludeTime();
await tester.selectDay(content: now.day);
await tester.dismissCellEditor();
@@ -4,12 +4,10 @@ import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/af_image.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
@@ -26,61 +24,6 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database row cover', () {
testWidgets('add image to media field and check if cover is set (grid)',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
// Change to media type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Media);
await tester.dismissFieldEditor();
// Prepare file for upload from local
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(paths: [imagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
// Open media cell editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
await tester.findMediaCellEditor(findsOneWidget);
// Click on add file button in the Media Cell Editor
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapFileUploadHint();
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
// Close cell editor
await tester.dismissCellEditor();
// Open first row in row detail view
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect a cover to be shown
expect(find.byType(RowCover), findsOneWidget);
// Remove the temp file
await Future.wait([file.delete()]);
});
testWidgets('add and remove cover from Row Detail Card', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -1,17 +1,18 @@
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/shared/icon_emoji_picker/recent_icons.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';
@@ -21,7 +22,14 @@ import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('grid row detail page:', () {
testWidgets('opens', (tester) async {
@@ -76,6 +84,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 {
@@ -368,11 +394,16 @@ void main() {
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 1);
tester.assertPhantomChecklistItemContent("");
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.hoverOnWidget(
find.byType(ChecklistRowDetailCell),
onHover: () async {
await tester.tapButton(find.byType(ChecklistItemControl));
},
);
tester.assertChecklistTaskInEditor(
index: 1,
@@ -380,6 +411,7 @@ void main() {
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 2);
tester.assertPhantomChecklistItemContent("");
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
@@ -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,67 @@
import 'dart:convert';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/icon/icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
testWidgets('callout with emoji icon picker', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final emojiIconData = await tester.loadIcon();
/// create a new document
await tester.createNewPageWithNameUnderParent();
/// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
/// create callout
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_callout.tr(),
);
/// select an icon
final emojiPickerButton = find.descendant(
of: find.byType(CalloutBlockComponentWidget),
matching: find.byType(EmojiPickerButton),
);
await tester.tapButton(emojiPickerButton);
await tester.tapIcon(emojiIconData);
/// verification results
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
final iconWidget = find
.descendant(
of: emojiPickerButton,
matching: find.byType(IconWidget),
)
.evaluate()
.first
.widget as IconWidget;
final iconWidgetData = iconWidget.iconsData;
expect(iconWidgetData.svgString, iconData.svgString);
expect(iconWidgetData.iconName, iconData.iconName);
expect(iconWidgetData.groupName, iconData.groupName);
});
}
@@ -1,7 +1,5 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
@@ -11,6 +9,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_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';
import 'package:universal_platform/universal_platform.dart';
@@ -27,10 +26,13 @@ void main() {
await tester.pasteContent(
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
(editorState) {
expect(editorState.document.root.children.length, 3);
expect(editorState.document.root.children.length, 1);
final text =
editorState.document.root.children.first.delta!.toPlainText();
final textLines = text.split('\n');
for (var i = 0; i < lines; i++) {
expect(
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
textLines[i],
'line $i',
);
}
@@ -467,6 +469,44 @@ void main() {
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});
const testMarkdownText = '''
# I'm h1
## I'm h2
### I'm h3
#### I'm h4
##### I'm h5
###### I'm h6''';
testWidgets('paste markdowns', (tester) async {
await tester.pasteContent(
plainText: testMarkdownText,
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (int i = 1; i <= children.length; i++) {
final text = children[i - 1].delta!.toPlainText();
expect(text, 'I\'m h$i');
}
},
);
});
testWidgets('paste markdowns as plain', (tester) async {
await tester.pasteContent(
plainText: testMarkdownText,
pasteAsPlain: true,
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (int i = 1; i <= children.length; i++) {
final text = children[i - 1].delta!.toPlainText();
final expectText = '${'#' * i} I\'m h$i';
expect(text, expectText);
}
},
);
});
}
extension on WidgetTester {
@@ -476,6 +516,7 @@ extension on WidgetTester {
String? plainText,
String? html,
String? inAppJson,
bool pasteAsPlain = false,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
@@ -502,6 +543,7 @@ extension on WidgetTester {
await simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isShiftPressed: pasteAsPlain,
isMetaPressed: Platform.isMacOS,
);
await pumpAndSettle();
@@ -0,0 +1,160 @@
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\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
"1234567\n\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
"12345678\n\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n\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,
);
/// 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);
/// press esc to dismiss the find menu
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(find.byType(FindAndReplaceMenuWidget), findsNothing);
},
);
}
@@ -2,12 +2,12 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.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/services.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -330,6 +330,23 @@ void main() {
expect(find.text("$_createdPageName (copy)"), findsNWidgets(2));
expect(find.text("$_createdPageName (copy) (copy)"), findsOneWidget);
});
testWidgets('Cancel inline page reference menu by space', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showPlusMenu();
// Cancel by space
await tester.simulateKeyEvent(
LogicalKeyboardKey.space,
);
await tester.pumpAndSettle();
expect(find.byType(InlineActionsMenu), findsNothing);
});
});
}
@@ -1,4 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -31,4 +37,104 @@ void main() {
expect(pageFinder, findsNWidgets(1));
});
});
testWidgets('count title towards word count', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
Finder title = tester.editor.findDocumentTitle('');
await tester.openMoreViewActions();
final viewMetaInfo = find.byType(ViewMetaInfo);
expect(viewMetaInfo, findsOneWidget);
ViewMetaInfo viewMetaInfoWidget =
viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
Counters titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, 0);
expect(titleCounter.wordCount, 0);
/// input [str1] within title
const str1 = 'Hello',
str2 = '$str1 AppFlowy',
str3 = '$str2!',
str4 = 'Hello world';
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.tapButton(title);
await tester.enterText(title, str1);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, str1.length);
expect(titleCounter.wordCount, 1);
/// input [str2] within title
title = tester.editor.findDocumentTitle(str1);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.tapButton(title);
await tester.enterText(title, str2);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, str2.length);
expect(titleCounter.wordCount, 2);
/// input [str3] within title
title = tester.editor.findDocumentTitle(str2);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.tapButton(title);
await tester.enterText(title, str3);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, str3.length);
expect(titleCounter.wordCount, 2);
/// input [str4] within document
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.editor
.updateSelection(Selection.collapsed(Position(path: [0])));
await tester.pumpAndSettle();
await tester.editor
.getCurrentEditorState()
.insertTextAtCurrentSelection(str4);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
final texts =
find.descendant(of: viewMetaInfo, matching: find.byType(FlowyText));
expect(texts, findsNWidgets(3));
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
final Counters documentCounters = viewMetaInfoWidget.documentCounters!;
final wordCounter = texts.evaluate().elementAt(0).widget as FlowyText,
charCounter = texts.evaluate().elementAt(1).widget as FlowyText;
final numberFormat = NumberFormat();
expect(
wordCounter.text,
LocaleKeys.moreAction_wordCount.tr(
args: [
numberFormat
.format(titleCounter.wordCount + documentCounters.wordCount)
.toString(),
],
),
);
expect(
charCounter.text,
LocaleKeys.moreAction_charCount.tr(
args: [
numberFormat
.format(
titleCounter.charCount + documentCounters.charCount,
)
.toString(),
],
),
);
});
}
@@ -1,17 +1,19 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
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/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
// Test cases for the Document SubPageBlock that needs to be covered:
@@ -38,7 +40,14 @@ import '../../shared/util.dart';
const _defaultPageName = "";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('Document SubPageBlock tests', () {
testWidgets('Insert a new SubPageBlock from Slash menu items',
@@ -49,11 +58,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
findsNWidgets(3),
@@ -68,12 +72,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -92,11 +90,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -145,11 +138,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -203,11 +191,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -244,11 +227,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -294,11 +272,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -337,11 +310,6 @@ void main() {
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -385,11 +353,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
@@ -412,12 +375,6 @@ void main() {
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@@ -438,11 +395,6 @@ void main() {
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
final beforeNode = tester.editor.getNodeAtPath([1]);
@@ -499,6 +451,43 @@ void main() {
expect(find.text('Parent'), findsNWidgets(2));
});
testWidgets('Displaying icon of subpage', (tester) async {
const firstPage = 'FirstPage';
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: firstPage);
final icon = await tester.loadIcon();
/// create subpage
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_subPage_name.tr(),
offset: 100,
);
/// add icon
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.tapIcon(icon);
await tester.pumpAndSettle();
await tester.openPage(firstPage);
await tester.expandOrCollapsePage(
pageName: firstPage,
layout: ViewLayoutPB.Document,
);
/// check if there is a icon in document
final iconWidget = find.byWidgetPredicate((w) {
if (w is! RawEmojiIconWidget) return false;
final iconData = w.emoji.emoji;
return iconData == icon.emoji;
});
expect(iconWidget, findsOneWidget);
});
});
}
@@ -1,13 +1,18 @@
import 'package:integration_test/integration_test.dart';
import 'document_block_option_test.dart' as document_block_option_test;
import 'document_find_menu_test.dart' as document_find_menu_test;
import 'document_inline_page_reference_test.dart'
as document_inline_page_reference_test;
import 'document_more_actions_test.dart' as document_more_actions_test;
import 'document_shortcuts_test.dart' as document_shortcuts_test;
import 'document_toolbar_test.dart' as document_toolbar_test;
import 'document_with_file_test.dart' as document_with_file_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
import 'document_with_simple_table_test.dart'
as document_with_simple_table_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -19,7 +24,8 @@ void main() {
document_more_actions_test.main();
document_with_file_test.main();
document_shortcuts_test.main();
// Disable subPage test temporarily, enable it in version 0.7.2
// document_sub_page_test.main();
document_block_option_test.main();
document_find_menu_test.main();
document_toolbar_test.main();
document_with_simple_table_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,
);
});
});
}
@@ -1,17 +1,34 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.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_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('cover image:', () {
testWidgets('document cover tests', (tester) async {
@@ -51,6 +68,59 @@ void main() {
tester.expectToSeeNoDocumentCover();
});
testWidgets('document cover local image tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
// Insert a document cover
await tester.editor.tapOnAddCover();
tester.expectToSeeDocumentCover(CoverType.asset);
// Hover over the cover to show the 'Change Cover' and delete buttons
await tester.editor.hoverOnCover();
tester.expectChangeCoverAndDeleteButton();
// Change cover to a local image image
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
await tester.editor.hoverOnCover();
await tester.editor.tapOnChangeCover();
final uploadButton = find.findTextInFlowyText(
LocaleKeys.document_imageBlock_upload_label.tr(),
);
await tester.tapButton(uploadButton);
mockPickFilePaths(paths: [localImagePath]);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
tester.expectToSeeDocumentCover(CoverType.file);
// Remove the cover
await tester.editor.hoverOnCover();
await tester.editor.tapOnRemoveCover();
tester.expectToSeeNoDocumentCover();
// Test if deleteImageFromLocalStorage(localImagePath) function is called once
await tester.pump(kDoubleTapTimeout);
expect(deleteImageTestCounter, 1);
// delete temp files
await imageFile.delete();
});
testWidgets('document icon tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -147,7 +217,7 @@ void main() {
tester.expectViewHasIcon(
gettingStarted,
ViewLayoutPB.Document,
punch,
EmojiIconData.emoji(punch),
);
});
});
@@ -1,12 +1,15 @@
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.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';
@@ -174,9 +177,110 @@ void main() {
findsOneWidget,
);
});
testWidgets('insert a referenced grid with many rows (load more option)',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
// validate the referenced grid is inserted
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(GridPage),
),
findsOneWidget,
);
// https://github.com/AppFlowy-IO/AppFlowy/issues/3533
// test: the selection of editor should be clear when editing the grid
await tester.editor.updateSelection(
Selection.collapsed(
Position(path: [1]),
),
);
final gridTextCell = find.byType(EditableTextCell).first;
await tester.tapButton(gridTextCell);
expect(tester.editor.getCurrentEditorState().selection, isNull);
final editorScrollable = find
.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byWidgetPredicate(
(w) => w is Scrollable && w.axis == Axis.vertical,
),
)
.first;
// Add 100 Rows to the linked database
final addRowFinder = find.byType(GridAddRowButton);
for (var i = 0; i < 100; i++) {
await tester.scrollUntilVisible(
addRowFinder,
100,
scrollable: editorScrollable,
);
await tester.tapButton(addRowFinder);
await tester.pumpAndSettle();
}
// Since all rows visible are those we added, we should see all of them
expect(find.byType(GridRow), findsNWidgets(103));
// Navigate to getting started
await tester.openPage(gettingStarted);
// Navigate back to the document
await tester.openPage('insert_a_reference_${ViewLayoutPB.Grid.name}');
// We see only 25 Grid Rows
expect(find.byType(GridRow), findsNWidgets(25));
// We see Add row and load more button
expect(find.byType(GridAddRowButton), findsOneWidget);
expect(find.byType(GridRowLoadMoreButton), findsOneWidget);
// Load more rows, expect 50 visible
await _loadMoreRows(tester, editorScrollable, 50);
// Load more rows, expect 75 visible
await _loadMoreRows(tester, editorScrollable, 75);
// Load more rows, expect 100 visible
await _loadMoreRows(tester, editorScrollable, 100);
// Load more rows, expect 103 visible
await _loadMoreRows(tester, editorScrollable, 103);
// We no longer see load more option
expect(find.byType(GridRowLoadMoreButton), findsNothing);
});
});
}
Future<void> _loadMoreRows(
WidgetTester tester,
Finder scrollable, [
int? expectedRows,
]) async {
await tester.scrollUntilVisible(
find.byType(GridRowLoadMoreButton),
100,
scrollable: scrollable,
);
await tester.pumpAndSettle();
await tester.tap(find.byType(GridRowLoadMoreButton));
await tester.pumpAndSettle();
if (expectedRows != null) {
expect(find.byType(GridRow), findsNWidgets(expectedRows));
}
}
/// Insert a referenced database of [layout] into the document
Future<void> insertLinkedDatabase(
WidgetTester tester,
@@ -1,14 +1,21 @@
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_date_block.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.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:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../shared/util.dart';
@@ -18,7 +25,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('date or reminder block in document', () {
group('date or reminder block in document:', () {
testWidgets("insert date with time block", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -121,5 +128,339 @@ void main() {
expect(find.text('@$formattedDate'), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
});
testWidgets("copy, cut and paste a date mention", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'copy, cut and paste a date mention',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
);
final dateTimeSettings = DateTimeSettingsPB(
dateFormat: UserDateFormatPB.Friendly,
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final DateTime currentDateTime = DateTime.now();
final String formattedDate =
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
// get current date in editor
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
// update selection and copy
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: 1),
),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
// update selection and paste
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(MentionDateBlock), findsNWidgets(2));
expect(find.text('@$formattedDate'), findsNWidgets(2));
// update selection and cut
await tester.editor.updateSelection(
Selection(
start: Position(path: [0], offset: 1),
end: Position(path: [0], offset: 2),
),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
// update selection and paste
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(MentionDateBlock), findsNWidgets(2));
expect(find.text('@$formattedDate'), findsNWidgets(2));
});
testWidgets("copy, cut and paste a reminder mention", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'copy, cut and paste a reminder mention',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
);
// trigger popup
await tester.tapButton(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// set date to be fifteenth of the next month
await tester.tap(
find.descendant(
of: find.byType(DesktopAppFlowyDatePicker),
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
),
);
await tester.pumpAndSettle();
await tester.tap(
find.descendant(
of: find.byType(TableCalendar),
matching: find.text(15.toString()),
),
);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// add a reminder
await tester.tap(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
await tester.pumpAndSettle();
await tester.tap(
find.textContaining(
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
),
);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// verify
final dateTimeSettings = DateTimeSettingsPB(
dateFormat: UserDateFormatPB.Friendly,
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final now = DateTime.now();
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
final formattedDate =
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
// update selection and copy
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: 1),
),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
// update selection and paste
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(MentionDateBlock), findsNWidgets(2));
expect(find.text('@$formattedDate'), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
expect(
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
2,
);
// update selection and cut
await tester.editor.updateSelection(
Selection(
start: Position(path: [0], offset: 1),
end: Position(path: [0], offset: 2),
),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
// update selection and paste
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(MentionDateBlock), findsNWidgets(2));
expect(find.text('@$formattedDate'), findsNWidgets(2));
expect(find.byType(MentionDateBlock), findsNWidgets(2));
expect(find.text('@$formattedDate'), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
expect(
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
2,
);
});
testWidgets("delete, undo and redo a reminder mention", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'delete, undo and redo a reminder mention',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
);
// trigger popup
await tester.tapButton(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// set date to be fifteenth of the next month
await tester.tap(
find.descendant(
of: find.byType(DesktopAppFlowyDatePicker),
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
),
);
await tester.pumpAndSettle();
await tester.tap(
find.descendant(
of: find.byType(TableCalendar),
matching: find.text(15.toString()),
),
);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// add a reminder
await tester.tap(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
await tester.pumpAndSettle();
await tester.tap(
find.textContaining(
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
),
);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// verify
final dateTimeSettings = DateTimeSettingsPB(
dateFormat: UserDateFormatPB.Friendly,
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final now = DateTime.now();
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
final formattedDate =
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
// update selection and backspace to delete the mention
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
await tester.pumpAndSettle();
expect(find.byType(MentionDateBlock), findsNothing);
expect(find.text('@$formattedDate'), findsNothing);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
// undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
// redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
isShiftPressed: true,
);
expect(find.byType(MentionDateBlock), findsNothing);
expect(find.text('@$formattedDate'), findsNothing);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
});
});
}
@@ -7,19 +7,16 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide UploadImageMenu, ResizableImage;
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';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:run_with_network_images/run_with_network_images.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
@@ -79,90 +76,6 @@ 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);
});
testWidgets('insert an image from unsplash', (tester) async {
await runWithNetworkImages(() 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);
expect(find.text('Unsplash'), findsOneWidget);
});
});
testWidgets('insert two images from local file at once', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -113,5 +113,55 @@ void main() {
tester.expectToSeeText(formula);
});
testWidgets('insert a inline math equation and type something after it',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'math equation',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
const formula = 'E = MC ^ 2';
await tester.ime.insertText(formula);
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// tap the inline math equation button
final inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block
final inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsOneWidget);
await tester.editor.tapLineOfEditorAt(0);
const text = 'Hello World';
await tester.ime.insertText(text);
final inlineText = find.textContaining(text, findRichText: true);
expect(inlineText, findsOneWidget);
// the text should be in the same line with the math equation
final inlineMathEquationPosition = tester.getRect(inlineMathEquation);
final textPosition = tester.getRect(inlineText);
// allow 5px difference
expect(
(textPosition.top - inlineMathEquationPosition.top).abs(),
lessThan(5),
);
expect(
(textPosition.bottom - inlineMathEquationPosition.bottom).abs(),
lessThan(5),
);
});
});
}
@@ -94,6 +94,20 @@ void main() {
await tester.tapButton(finder);
expect(find.byType(GridPage), findsOneWidget);
});
testWidgets('insert a inline page and type something after the page',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertInlinePage(tester, ViewLayoutPB.Grid);
await tester.editor.tapLineOfEditorAt(0);
const text = 'Hello World';
await tester.ime.insertText(text);
expect(find.textContaining(text, findRichText: true), findsOneWidget);
});
});
}
@@ -48,7 +48,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
offset: 80,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
@@ -146,7 +146,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
offset: 80,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
@@ -1,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),
);
//////
});
@@ -186,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,783 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../shared/util.dart';
const String heading1 = "Heading 1";
const String heading2 = "Heading 2";
const String heading3 = "Heading 3";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('simple table block test:', () {
testWidgets('insert a simple table block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// validate the table is inserted
expect(find.byType(SimpleTableBlockWidget), findsOneWidget);
final editorState = tester.editor.getCurrentEditorState();
expect(
editorState.selection,
// table -> row -> cell -> paragraph
Selection.collapsed(Position(path: [0, 0, 0, 0])),
);
final firstCell = find.byType(SimpleTableCellBlockWidget).first;
expect(
tester
.state<SimpleTableCellBlockWidgetState>(firstCell)
.isEditingCellNotifier
.value,
isTrue,
);
});
testWidgets('select all in table cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
const cell1Content = 'Cell 1';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('New Table');
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
await tester.editor.tapLineOfEditorAt(1);
await tester.insertTableInDocument();
await tester.ime.insertText(cell1Content);
await tester.pumpAndSettle();
// Select all in the cell
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection(
start: Position(path: [1, 0, 0, 0]),
end: Position(path: [1, 0, 0, 0], offset: cell1Content.length),
),
);
// Press select all again, the selection should be the entire document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection(
start: Position(path: [0]),
end: Position(path: [1, 1, 1, 0]),
),
);
});
testWidgets('''
1. hover on the table
1.1 click the add row button
1.2 click the add column button
1.3 click the add row and column button
2. validate the table is updated
3. delete the last column
4. delete the last row
5. validate the table is updated
''', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// add a new row
final row = find.byWidgetPredicate((w) {
return w is SimpleTableRowBlockWidget && w.node.rowIndex == 1;
});
await tester.hoverOnWidget(
row,
onHover: () async {
final addRowButton = find.byType(SimpleTableAddRowButton).first;
await tester.tap(addRowButton);
},
);
await tester.pumpAndSettle();
// add a new column
final column = find.byWidgetPredicate((w) {
return w is SimpleTableCellBlockWidget && w.node.columnIndex == 1;
}).first;
await tester.hoverOnWidget(
column,
onHover: () async {
final addColumnButton = find.byType(SimpleTableAddColumnButton).first;
await tester.tap(addColumnButton);
},
);
await tester.pumpAndSettle();
// add a new row and a new column
final row2 = find.byWidgetPredicate((w) {
return w is SimpleTableCellBlockWidget &&
w.node.rowIndex == 2 &&
w.node.columnIndex == 2;
}).first;
await tester.hoverOnWidget(
row2,
onHover: () async {
// click the add row and column button
final addRowAndColumnButton =
find.byType(SimpleTableAddColumnAndRowButton).first;
await tester.tap(addRowAndColumnButton);
},
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 4);
expect(tableNode.rowLength, 4);
// delete the last row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: tableNode.rowLength - 1,
action: SimpleTableMoreAction.delete,
);
await tester.pumpAndSettle();
expect(tableNode.rowLength, 3);
expect(tableNode.columnLength, 4);
// delete the last column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: tableNode.columnLength - 1,
action: SimpleTableMoreAction.delete,
);
await tester.pumpAndSettle();
expect(tableNode.columnLength, 3);
expect(tableNode.rowLength, 3);
});
testWidgets('enable header column and header row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// enable the header row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.enableHeaderRow,
);
await tester.pumpAndSettle();
// enable the header column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.enableHeaderColumn,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.isHeaderColumnEnabled, isTrue);
expect(tableNode.isHeaderRowEnabled, isTrue);
// disable the header row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.enableHeaderRow,
);
await tester.pumpAndSettle();
expect(tableNode.isHeaderColumnEnabled, isTrue);
expect(tableNode.isHeaderRowEnabled, isFalse);
// disable the header column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.enableHeaderColumn,
);
await tester.pumpAndSettle();
expect(tableNode.isHeaderColumnEnabled, isFalse);
expect(tableNode.isHeaderRowEnabled, isFalse);
});
testWidgets('duplicate a column / row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// duplicate the row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.duplicate,
);
await tester.pumpAndSettle();
// duplicate the column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.duplicate,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 3);
expect(tableNode.rowLength, 3);
});
testWidgets('insert left / insert right', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// insert left
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.insertLeft,
);
await tester.pumpAndSettle();
// insert right
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.insertRight,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 4);
expect(tableNode.rowLength, 2);
});
testWidgets('insert above / insert below', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// insert above
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.insertAbove,
);
await tester.pumpAndSettle();
// insert below
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.insertBelow,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.rowLength, 4);
expect(tableNode.columnLength, 2);
});
});
testWidgets('set column width to page width (1)', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
final tableNode = tester.editor.getNodeAtPath([0]);
final beforeWidth = tableNode.width;
// set the column width to page width
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.setToPageWidth,
);
await tester.pumpAndSettle();
final afterWidth = tableNode.width;
expect(afterWidth, greaterThan(beforeWidth));
});
testWidgets('set column width to page width (2)', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
final tableNode = tester.editor.getNodeAtPath([0]);
final beforeWidth = tableNode.width;
// set the column width to page width
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.setToPageWidth,
);
await tester.pumpAndSettle();
final afterWidth = tableNode.width;
expect(afterWidth, greaterThan(beforeWidth));
});
testWidgets('distribute columns evenly (1)', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
final tableNode = tester.editor.getNodeAtPath([0]);
final beforeWidth = tableNode.width;
// set the column width to page width
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.distributeColumnsEvenly,
);
await tester.pumpAndSettle();
final afterWidth = tableNode.width;
expect(afterWidth, equals(beforeWidth));
final distributeColumnWidthsEvenly =
tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly];
expect(distributeColumnWidthsEvenly, isTrue);
});
testWidgets('distribute columns evenly (2)', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
final tableNode = tester.editor.getNodeAtPath([0]);
final beforeWidth = tableNode.width;
// set the column width to page width
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.distributeColumnsEvenly,
);
await tester.pumpAndSettle();
final afterWidth = tableNode.width;
expect(afterWidth, equals(beforeWidth));
final distributeColumnWidthsEvenly =
tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly];
expect(distributeColumnWidthsEvenly, isTrue);
});
testWidgets('using option menu to set column width', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
await tester.editor.hoverAndClickOptionMenuButton([0]);
final editorState = tester.editor.getCurrentEditorState();
final beforeWidth = editorState.document.nodeAtPath([0])!.width;
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth.tr(),
),
);
await tester.pumpAndSettle();
final afterWidth = editorState.document.nodeAtPath([0])!.width;
expect(afterWidth, greaterThan(beforeWidth));
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButton(
find.text(
LocaleKeys
.document_plugins_simpleTable_moreActions_distributeColumnsWidth
.tr(),
),
);
await tester.pumpAndSettle();
final afterWidth2 = editorState.document.nodeAtPath([0])!.width;
expect(afterWidth2, equals(afterWidth));
});
testWidgets('insert a table and use select all the delete it',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
await tester.editor.tapLineOfEditorAt(1);
await tester.ime.insertText('Hello World');
// select all
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isMetaPressed: UniversalPlatform.isMacOS,
isControlPressed: !UniversalPlatform.isMacOS,
);
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
// only one paragraph left
expect(editorState.document.root.children.length, 1);
final paragraphNode = editorState.document.nodeAtPath([0])!;
expect(paragraphNode.delta, isNull);
});
testWidgets('use tab or shift+tab to navigate in table', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
await tester.simulateKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final selection = editorState.selection;
expect(selection, isNotNull);
expect(selection!.start.path, [0, 0, 1, 0]);
expect(selection.end.path, [0, 0, 1, 0]);
await tester.simulateKeyEvent(
LogicalKeyboardKey.tab,
isShiftPressed: true,
);
await tester.pumpAndSettle();
final selection2 = editorState.selection;
expect(selection2, isNotNull);
expect(selection2!.start.path, [0, 0, 0, 0]);
expect(selection2.end.path, [0, 0, 0, 0]);
});
testWidgets('shift+enter to insert a new line in table', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
await tester.simulateKeyEvent(
LogicalKeyboardKey.enter,
isShiftPressed: true,
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.document.nodeAtPath([0, 0, 0])!;
expect(node.children.length, 1);
});
testWidgets('using option menu to set table align', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
await tester.editor.hoverAndClickOptionMenuButton([0]);
final editorState = tester.editor.getCurrentEditorState();
final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;
expect(beforeAlign, TableAlign.left);
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
),
);
await tester.pumpAndSettle();
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_optionAction_center.tr(),
),
);
await tester.pumpAndSettle();
final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;
expect(afterAlign, TableAlign.center);
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
),
);
await tester.pumpAndSettle();
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_optionAction_right.tr(),
),
);
await tester.pumpAndSettle();
final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;
expect(afterAlign2, TableAlign.right);
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
),
);
await tester.pumpAndSettle();
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_optionAction_left.tr(),
),
);
await tester.pumpAndSettle();
final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;
expect(afterAlign3, TableAlign.left);
});
testWidgets('using option menu to set table align', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
await tester.editor.hoverAndClickOptionMenuButton([0]);
final editorState = tester.editor.getCurrentEditorState();
final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;
expect(beforeAlign, TableAlign.left);
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
),
);
await tester.pumpAndSettle();
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_optionAction_center.tr(),
),
);
await tester.pumpAndSettle();
final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;
expect(afterAlign, TableAlign.center);
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
),
);
await tester.pumpAndSettle();
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_optionAction_right.tr(),
),
);
await tester.pumpAndSettle();
final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;
expect(afterAlign2, TableAlign.right);
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
),
);
await tester.pumpAndSettle();
await tester.tapButton(
find.text(
LocaleKeys.document_plugins_optionAction_left.tr(),
),
);
await tester.pumpAndSettle();
final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;
expect(afterAlign3, TableAlign.left);
});
testWidgets('support slash menu in table', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
final editorState = tester.editor.getCurrentEditorState();
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
final path = [0, 0, 0, 0];
final selection = Selection.collapsed(Position(path: path));
editorState.selection = selection;
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
final paragraphItem = find.byWidgetPredicate((w) {
return w is SelectionMenuItemWidget &&
w.item.name == LocaleKeys.document_slashMenu_name_text.tr();
});
expect(paragraphItem, findsOneWidget);
await tester.tap(paragraphItem);
await tester.pumpAndSettle();
final paragraphNode = editorState.document.nodeAtPath(path)!;
expect(paragraphNode.type, equals(ParagraphBlockKeys.type));
});
}
extension on WidgetTester {
/// Insert a table in the document
Future<void> insertTableInDocument() async {
// open the actions menu and insert the outline block
await editor.showSlashMenu();
await editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_table.tr(),
);
await pumpAndSettle();
}
Future<void> clickMoreActionItemInTableMenu({
required SimpleTableMoreActionType type,
required int index,
required SimpleTableMoreAction action,
}) async {
if (type == SimpleTableMoreActionType.row) {
final row = find.byWidgetPredicate((w) {
return w is SimpleTableRowBlockWidget && w.node.rowIndex == index;
});
await hoverOnWidget(
row,
onHover: () async {
final moreActionButton = find.byWidgetPredicate((w) {
return w is SimpleTableMoreActionMenu &&
w.type == SimpleTableMoreActionType.row &&
w.index == index;
});
await tapButton(moreActionButton);
await tapButton(find.text(action.name));
},
);
await pumpAndSettle();
} else if (type == SimpleTableMoreActionType.column) {
final column = find.byWidgetPredicate((w) {
return w is SimpleTableCellBlockWidget && w.node.columnIndex == index;
}).first;
await hoverOnWidget(
column,
onHover: () async {
final moreActionButton = find.byWidgetPredicate((w) {
return w is SimpleTableMoreActionMenu &&
w.type == SimpleTableMoreActionType.column &&
w.index == index;
});
await tapButton(moreActionButton);
await tapButton(find.text(action.name));
},
);
await pumpAndSettle();
}
await tapAt(Offset.zero);
}
}
@@ -1,7 +1,9 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -263,5 +265,24 @@ void main() {
expect(node.attributes[ToggleListBlockKeys.level], 3);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('click the toggle list to create a new paragraph',
(tester) async {
await prepareToggleHeadingBlock(tester, '> # Hello');
final emptyHintText = find.text(
LocaleKeys.document_plugins_emptyToggleHeading.tr(
args: ['1'],
),
);
expect(emptyHintText, findsOneWidget);
await tester.tapButton(emptyHintText);
await tester.pumpAndSettle();
// check the new paragraph is created
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0, 0])!;
expect(node.type, ParagraphBlockKeys.type);
});
});
}
@@ -35,10 +35,12 @@ void main() {
await tester.pumpAndSettle();
await tester.hoverOnWidget(
find.descendant(
of: find.byType(ShortcutSettingTile),
matching: find.text(backspaceCmd),
),
find
.descendant(
of: find.byType(ShortcutSettingTile),
matching: find.text(backspaceCmd),
)
.first,
onHover: () async {
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
await tester.pumpAndSettle();
@@ -2,10 +2,10 @@ import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:toastification/toastification.dart';
import '../../shared/util.dart';
@@ -23,7 +23,7 @@ void main() {
.last;
}
group('sign-in page settings: ', () {
group('sign-in page settings:', () {
testWidgets('change server type', (tester) async {
await tester.initializeAppFlowy();
@@ -45,28 +45,36 @@ void main() {
// change the server type to self-host
await tester.tapButton(appflowyCloudType);
final selfhostedButton = findServerType(
final selfHostedButton = findServerType(
AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapButton(selfhostedButton);
await tester.tapButton(selfHostedButton);
// update server url
const serverUrl = 'https://test.appflowy.cloud';
const serverUrl = 'https://self-hosted.appflowy.cloud';
await tester.enterText(
find.byKey(kSelfHostedTextInputFieldKey),
serverUrl,
);
await tester.pumpAndSettle();
// update the web url
const webUrl = 'https://self-hosted.appflowy.com';
await tester.enterText(
find.byKey(kSelfHostedWebTextInputFieldKey),
webUrl,
);
await tester.pumpAndSettle();
await tester.tapButton(
find.findTextInFlowyText(LocaleKeys.button_save.tr()),
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpUntilNotFound(find.byType(DesktopToast));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// open settings page to check the result
await tester.tapButton(settingsButton);
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// check the server type
expect(
@@ -78,18 +86,23 @@ void main() {
find.text(serverUrl),
findsOneWidget,
);
// check the web url
expect(
find.text(webUrl),
findsOneWidget,
);
// reset to appflowy cloud
await tester.tapButton(
findServerType(AuthenticatorType.appflowyCloudSelfHost),
);
);
// change the server type to appflowy cloud
await tester.tapButton(
findServerType(AuthenticatorType.appflowyCloud),
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpUntilNotFound(find.byType(DesktopToast));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// check the server type
@@ -1,8 +1,12 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -44,5 +48,82 @@ void main() {
);
expect(isExpanded(type: FolderSpaceType.private), true);
});
testWidgets('Expanding with subpage', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const page1 = 'SubPageBloc', page2 = '$page1 2';
await tester.createNewPageWithNameUnderParent(name: page1);
await tester.createNewPageWithNameUnderParent(
name: page2,
parentName: page1,
);
await tester.expandOrCollapsePage(
pageName: gettingStarted,
layout: ViewLayoutPB.Document,
);
await tester.tapNewPageButton();
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
final slashMenu = find
.ancestor(
of: find.byType(SelectionMenuItemWidget),
matching: find.byWidgetPredicate(
(widget) => widget is Scrollable,
),
)
.first;
final slashMenuItem = find.text(
LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
);
await tester.scrollUntilVisible(
slashMenuItem,
100,
scrollable: slashMenu,
duration: const Duration(milliseconds: 250),
);
final menuItemFinder = find.byWidgetPredicate(
(w) =>
w is SelectionMenuItemWidget &&
w.item.name == LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
);
final menuItem =
menuItemFinder.evaluate().first.widget as SelectionMenuItemWidget;
/// tapSlashMenuItemWithName is not working, so invoke this function directly
menuItem.item.handler(
menuItem.editorState,
menuItem.menuService,
menuItemFinder.evaluate().first,
);
await tester.pumpAndSettle();
final actionHandler = find.byType(InlineActionsHandler);
final subPage = find.descendant(
of: actionHandler,
matching: find.text(page2, findRichText: true),
);
await tester.tapButton(subPage);
final subpageBlock = find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.text(page2, findRichText: true),
);
expect(find.text(page2, findRichText: true), findsOneWidget);
await tester.tapButton(subpageBlock);
/// one is in SectionFolder, another one is in CoverTitle
/// the last one is in FlowyNavigation
expect(find.text(page2, findRichText: true), findsNWidgets(3));
});
});
}
@@ -196,5 +196,58 @@ void main() {
await tester.pumpAndSettle();
},
);
testWidgets(
'reorder favorites',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// there are no favorite views
final favorites = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.byType(ViewItem),
);
expect(favorites, findsNothing);
/// create views and then favorite them
const pageNames = ['001', '002', '003'];
for (final name in pageNames) {
await tester.createNewPageWithNameUnderParent(name: name);
}
for (final name in pageNames) {
await tester.favoriteViewByName(name);
}
expect(favorites, findsNWidgets(pageNames.length));
final oldNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(oldNames, pageNames);
/// drag first to last
await tester.reorderFavorite(
fromName: '001',
toName: '003',
);
List<String> newNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(newNames, ['002', '003', '001']);
/// drag first to second
await tester.reorderFavorite(
fromName: '002',
toName: '003',
);
newNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(newNames, ['003', '002', '001']);
},
);
});
}
@@ -1,83 +1,212 @@
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/style_widget/text_field.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/emoji.dart';
import '../../shared/expectation.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final emoji = EmojiIconData.emoji('😁');
const emoji = '😁';
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
group('Icon:', () {
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
tearDownAll(() {
RecentIcons.enable = true;
});
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
testWidgets('Update page emoji in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// update its icon
await tester.updatePageIconInSidebarByName(
name: value.name,
parentName: gettingStarted,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
});
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
testWidgets('Update page icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// update its emoji
await tester.updatePageIconInSidebarByName(
name: value.name,
parentName: gettingStarted,
layout: value,
icon: emoji,
);
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
}
});
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
testWidgets('Update page emoji in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
tester.expectViewTitleHasIcon(
value.name,
value,
emoji,
);
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
});
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its emoji
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
tester.expectViewTitleHasIcon(
value.name,
value,
emoji,
);
}
});
testWidgets('Emoji Search Bar Get Focus', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
await tester.openPage(
value.name,
layout: value,
);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(value.name),
);
await tester.tapButton(title);
await tester.tapButton(find.byType(EmojiPickerButton));
final emojiPicker = find.byType(FlowyEmojiPicker);
expect(emojiPicker, findsOneWidget);
final textField = find.descendant(
of: emojiPicker,
matching: find.byType(FlowyTextField),
);
expect(textField, findsOneWidget);
final textFieldWidget =
textField.evaluate().first.widget as FlowyTextField;
assert(textFieldWidget.focusNode!.hasFocus);
await tester.tapEmoji(emoji.emoji);
await tester.pumpAndSettle();
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
}
});
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInSidebarByName(
name: value.name,
parentName: gettingStarted,
layout: value,
icon: iconData,
);
tester.expectViewHasIcon(
value.name,
value,
iconData,
);
}
});
testWidgets('Update page icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: iconData,
);
tester.expectViewHasIcon(
value.name,
value,
iconData,
);
tester.expectViewTitleHasIcon(
value.name,
value,
iconData,
);
}
});
}
@@ -0,0 +1,56 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/expectation.dart';
void main() {
testWidgets('Skip the empty group name icon in recent icons', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// clear local data
RecentIcons.clear();
await loadIconGroups();
final groups = kIconGroups!;
final List<RecentIcon> localIcons = [];
for (final e in groups) {
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
}
await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, ''));
await tester.openPage(gettingStarted);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(gettingStarted),
);
await tester.tapButton(title);
/// tap emoji picker button
await tester.tapButton(find.byType(EmojiPickerButton));
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
/// tap icon tab
final pickTab = find.byType(PickerTab);
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.icon.tr),
);
await tester.tapButton(iconTab);
expect(find.byType(FlowyIconPicker), findsOneWidget);
/// no recent icons
final recentText = find.descendant(
of: find.byType(FlowyIconPicker),
matching: find.text('Recent'),
);
expect(recentText, findsNothing);
});
}
@@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_recent_icon_test.dart' as sidebar_recent_icon_test;
import 'sidebar_test.dart' as sidebar_test;
import 'sidebar_view_item_test.dart' as sidebar_view_item_test;
@@ -14,4 +15,5 @@ void main() {
sidebar_favorite_test.main();
sidebar_icon_test.main();
sidebar_view_item_test.main();
sidebar_recent_icon_test.main();
}
@@ -1,5 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -11,7 +13,14 @@ import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('Sidebar view item tests', () {
testWidgets('Access view item context menu by right click', (tester) async {
@@ -38,7 +47,11 @@ void main() {
await tester.tapEmoji(emoji);
await tester.pumpAndSettle();
tester.expectViewHasIcon(gettingStarted, ViewLayoutPB.Document, emoji);
tester.expectViewHasIcon(
gettingStarted,
ViewLayoutPB.Document,
EmojiIconData.emoji(emoji),
);
});
});
}
@@ -0,0 +1,91 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_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';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/document_test_operations.dart';
import '../document/document_codeblock_paste_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Code Block Language Selector 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));
expect(find.byType(CodeBlockLanguageSelector), findsNothing);
await insertCodeBlockInDocument(tester);
///tap button
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
await tester
.tapButtonWithName(LocaleKeys.document_codeBlock_language_auto.tr());
expect(find.byType(CodeBlockLanguageSelector), findsOneWidget);
for (var i = 0; i < 3; ++i) {
await onKey(tester, LogicalKeyboardKey.arrowDown);
}
for (var i = 0; i < 2; ++i) {
await onKey(tester, LogicalKeyboardKey.arrowUp);
}
await onKey(tester, LogicalKeyboardKey.enter);
final editorState = tester.editor.getCurrentEditorState();
String language = editorState
.getNodeAtPath([0])!
.attributes[CodeBlockKeys.language]
.toString();
expect(
language.toLowerCase(),
defaultCodeBlockSupportedLanguages.first.toLowerCase(),
);
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
await tester.tapButtonWithName(language);
await onKey(tester, LogicalKeyboardKey.arrowUp);
await onKey(tester, LogicalKeyboardKey.enter);
language = editorState
.getNodeAtPath([0])!
.attributes[CodeBlockKeys.language]
.toString();
expect(
language.toLowerCase(),
defaultCodeBlockSupportedLanguages.last.toLowerCase(),
);
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
await tester.tapButtonWithName(language);
tester.testTextInput.enterText("rust");
await onKey(tester, LogicalKeyboardKey.delete);
await onKey(tester, LogicalKeyboardKey.delete);
await onKey(tester, LogicalKeyboardKey.arrowDown);
tester.testTextInput.enterText("st");
await onKey(tester, LogicalKeyboardKey.arrowDown);
await onKey(tester, LogicalKeyboardKey.enter);
language = editorState
.getNodeAtPath([0])!
.attributes[CodeBlockKeys.language]
.toString();
expect(language.toLowerCase(), 'rust');
});
}
Future<void> onKey(WidgetTester tester, LogicalKeyboardKey key) async {
await tester.simulateKeyEvent(key);
await tester.pumpAndSettle();
}
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -87,7 +88,7 @@ void main() {
);
expect(
importedPageEditorState.getNodeAtPath([2])!.type,
TableBlockKeys.type,
SimpleTableBlockKeys.type,
);
});
});
@@ -1,17 +1,23 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_svg/flowy_svg.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 +26,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 +45,7 @@ void main() {
expect(
find.descendant(
of: find.byType(TabBar),
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(3),
@@ -71,11 +72,300 @@ 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));
});
testWidgets('cannot close pinned tabs', (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),
);
const firstTab = _documentTwoName;
const secondTab = gettingStarted;
const thirdTab = _documentName;
expect(tester.isTabAtIndex(firstTab, 0), isTrue);
expect(tester.isTabAtIndex(secondTab, 1), isTrue);
expect(tester.isTabAtIndex(thirdTab, 2), isTrue);
expect(tester.isTabPinned(gettingStarted), isFalse);
// Right click on second tab
await tester.openTabMenu(gettingStarted);
expect(find.byType(TabMenu), findsOneWidget);
// Pin second tab
await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));
await tester.pumpAndSettle();
expect(tester.isTabPinned(gettingStarted), isTrue);
/// Right click on first unpinned tab (second tab)
await tester.openTabMenu(_documentTwoName);
// Close others
await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr()));
await tester.pumpAndSettle();
// We expect to find 2 tabs, the first pinned tab and the second tab
expect(find.byType(FlowyTab), findsNWidgets(2));
expect(tester.isTabAtIndex(gettingStarted, 0), isTrue);
expect(tester.isTabAtIndex(_documentTwoName, 1), isTrue);
});
testWidgets('pin/unpin tabs proper order', (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),
);
const firstTabName = _documentTwoName;
const secondTabName = gettingStarted;
const thirdTabName = _documentName;
// Expect correct order
expect(tester.isTabAtIndex(firstTabName, 0), isTrue);
expect(tester.isTabAtIndex(secondTabName, 1), isTrue);
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
// Pin second tab
await tester.openTabMenu(secondTabName);
await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));
await tester.pumpAndSettle();
expect(tester.isTabPinned(secondTabName), isTrue);
// Expect correct order
expect(tester.isTabAtIndex(secondTabName, 0), isTrue);
expect(tester.isTabAtIndex(firstTabName, 1), isTrue);
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
// Pin new second tab (first tab)
await tester.openTabMenu(firstTabName);
await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));
await tester.pumpAndSettle();
expect(tester.isTabPinned(firstTabName), isTrue);
expect(tester.isTabPinned(secondTabName), isTrue);
expect(tester.isTabPinned(thirdTabName), isFalse);
expect(tester.isTabAtIndex(secondTabName, 0), isTrue);
expect(tester.isTabAtIndex(firstTabName, 1), isTrue);
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
// Unpin second tab
await tester.openTabMenu(secondTabName);
await tester.tap(find.text(LocaleKeys.tabMenu_unpinTab.tr()));
await tester.pumpAndSettle();
expect(tester.isTabPinned(firstTabName), isTrue);
expect(tester.isTabPinned(secondTabName), isFalse);
expect(tester.isTabPinned(thirdTabName), isFalse);
expect(tester.isTabAtIndex(firstTabName, 0), isTrue);
expect(tester.isTabAtIndex(secondTabName, 1), isTrue);
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
});
testWidgets('displaying icons in tab', (tester) async {
RecentIcons.enable = false;
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final icon = await tester.loadIcon();
// update emoji
await tester.updatePageIconInSidebarByName(
name: gettingStarted,
parentName: gettingStarted,
layout: ViewLayoutPB.Document,
icon: icon,
);
/// create new page
await tester.createNewPageWithNameUnderParent(name: _documentName);
/// open new tab for [gettingStarted]
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
final tabs = find.descendant(
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
);
expect(tabs, findsNWidgets(2));
final svgInTab =
find.descendant(of: tabs.last, matching: find.byType(FlowySvg));
final svgWidget = svgInTab.evaluate().first.widget as FlowySvg;
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
expect(svgWidget.svgString, iconsData.svgString);
});
});
}
extension _TabsTester on WidgetTester {
bool isTabPinned(String tabName) {
final tabFinder = find.ancestor(
of: find.byWidgetPredicate(
(w) => w is ViewTabBarItem && w.view.name == tabName,
),
matching: find.byType(FlowyTab),
);
final FlowyTab tabWidget = widget(tabFinder);
return tabWidget.pageManager.isPinned;
}
bool isTabAtIndex(String tabName, int index) {
final tabFinder = find.ancestor(
of: find.byWidgetPredicate(
(w) => w is ViewTabBarItem && w.view.name == tabName,
),
matching: find.byType(FlowyTab),
);
final pluginId = (widget(tabFinder) as FlowyTab).pageManager.plugin.id;
final pluginIds = find
.byType(FlowyTab)
.evaluate()
.map((e) => (e.widget as FlowyTab).pageManager.plugin.id);
return pluginIds.elementAt(index) == pluginId;
}
Future<void> openTabMenu(String tabName) async {
await tap(
buttons: kSecondaryButton,
find.ancestor(
of: find.byWidgetPredicate(
(w) => w is ViewTabBarItem && w.view.name == tabName,
),
matching: find.byType(FlowyTab),
),
);
await pumpAndSettle();
}
}
@@ -4,7 +4,6 @@ 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 'tabs_test.dart' as tabs_test;
import 'zoom_in_out_test.dart' as zoom_in_out_test;
void main() {
@@ -17,7 +16,6 @@ void main() {
emoji_shortcut_test.main();
share_markdown_test.main();
import_files_test.main();
tabs_test.main();
zoom_in_out_test.main();
// DON'T add more tests here.
}
@@ -0,0 +1,18 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/uncategorized/code_block_language_selector_test.dart'
as code_language_selector;
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();
code_language_selector.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,7 +1,10 @@
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 'simple_table_test.dart' as simple_table_test;
import 'title_test.dart' as title_test;
import 'toolbar_test.dart' as toolbar_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -9,4 +12,7 @@ void main() {
// Document integration tests
title_test.main();
page_style_test.main();
plus_menu_test.main();
simple_table_test.main();
toolbar_test.main();
}
@@ -1,42 +1,32 @@
// 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/mobile_bottom_navigation_bar.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/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.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/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
group('document page style', () {
tearDownAll(() {
RecentIcons.enable = true;
});
group('document page style:', () {
double getCurrentEditorFontSize() {
final editorPage = find
.byType(AppFlowyEditorPage)
@@ -57,11 +47,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 +75,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,
);
});
@@ -135,5 +127,37 @@ void main() {
);
expect(builtInCover, findsOneWidget);
});
testWidgets('page style icon', (tester) async {
await tester.launchInAnonymousMode();
final createPageButton =
find.byKey(BottomNavigationBarItemType.add.valueKey);
await tester.tapButton(createPageButton);
/// toggle the preset button
await tester.tapSvgButton(FlowySvgs.m_layout_s);
/// select document plugins emoji
final pageStyleIcon = find.byType(PageStyleIcon);
/// there should be none of emoji
final noneText = find.text(LocaleKeys.pageStyle_none.tr());
expect(noneText, findsOneWidget);
await tester.tapButton(pageStyleIcon);
/// select an emoji
const emoji = '😄';
await tester.tapEmoji(emoji);
await tester.tapSvgButton(FlowySvgs.m_layout_s);
expect(noneText, findsNothing);
expect(
find.descendant(
of: pageStyleIcon,
matching: find.text(emoji),
),
findsOneWidget,
);
});
});
}
@@ -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]))),
);
});
});
}
@@ -0,0 +1,554 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('simple table:', () {
testWidgets('''
1. insert a simple table via + menu
2. insert a row above the table
3. insert a row below the table
4. insert a column left to the table
5. insert a column right to the table
6. delete the first row
7. delete the first column
''', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('simple table');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
final firstParagraphPath = [0, 0, 0, 0];
// open the plus menu and select the table block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_table.tr(),
);
// check the block is inserted
final table = editorState.getNodeAtPath([0])!;
expect(table.type, equals(SimpleTableBlockKeys.type));
expect(table.rowLength, equals(2));
expect(table.columnLength, equals(2));
// focus on the first cell
final selection = editorState.selection!;
expect(selection.isCollapsed, isTrue);
expect(selection.start.path, equals(firstParagraphPath));
}
// insert left and insert right
{
// click the column menu button
await tester.clickColumnMenuButton(0);
// insert left, insert right
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_insertLeft.tr(),
),
);
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_insertRight
.tr(),
),
);
await tester.cancelTableActionMenu();
// check the table is updated
final table = editorState.getNodeAtPath([0])!;
expect(table.type, equals(SimpleTableBlockKeys.type));
expect(table.rowLength, equals(2));
expect(table.columnLength, equals(4));
}
// insert above and insert below
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickRowMenuButton(0);
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_insertAbove
.tr(),
),
);
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_insertBelow
.tr(),
),
);
await tester.cancelTableActionMenu();
// check the table is updated
final table = editorState.getNodeAtPath([0])!;
expect(table.rowLength, equals(4));
expect(table.columnLength, equals(4));
}
// delete the first row
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// delete the first row
await tester.clickRowMenuButton(0);
await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);
await tester.cancelTableActionMenu();
// check the table is updated
final table = editorState.getNodeAtPath([0])!;
expect(table.rowLength, equals(3));
expect(table.columnLength, equals(4));
}
// delete the first column
{
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
await tester.clickColumnMenuButton(0);
await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);
await tester.cancelTableActionMenu();
// check the table is updated
final table = editorState.getNodeAtPath([0])!;
expect(table.rowLength, equals(3));
expect(table.columnLength, equals(3));
}
});
testWidgets('''
1. insert a simple table via + menu
2. enable header column
3. enable header row
4. set to page width
5. distribute columns evenly
''', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('simple table');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
final firstParagraphPath = [0, 0, 0, 0];
// open the plus menu and select the table block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_table.tr(),
);
// check the block is inserted
final table = editorState.getNodeAtPath([0])!;
expect(table.type, equals(SimpleTableBlockKeys.type));
expect(table.rowLength, equals(2));
expect(table.columnLength, equals(2));
// focus on the first cell
final selection = editorState.selection!;
expect(selection.isCollapsed, isTrue);
expect(selection.start.path, equals(firstParagraphPath));
}
// enable header column
{
// click the column menu button
await tester.clickColumnMenuButton(0);
// enable header column
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_headerColumn
.tr(),
),
);
}
// enable header row
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickRowMenuButton(0);
// enable header column
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_headerRow.tr(),
),
);
}
// check the table is updated
final table = editorState.getNodeAtPath([0])!;
expect(table.type, equals(SimpleTableBlockKeys.type));
expect(table.isHeaderColumnEnabled, isTrue);
expect(table.isHeaderRowEnabled, isTrue);
// disable header column
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickColumnMenuButton(0);
final toggleButton = find.descendant(
of: find.byType(SimpleTableHeaderActionButton),
matching: find.byType(CupertinoSwitch),
);
await tester.tapButton(toggleButton);
}
// enable header row
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickRowMenuButton(0);
// enable header column
final toggleButton = find.descendant(
of: find.byType(SimpleTableHeaderActionButton),
matching: find.byType(CupertinoSwitch),
);
await tester.tapButton(toggleButton);
}
// check the table is updated
expect(table.isHeaderColumnEnabled, isFalse);
expect(table.isHeaderRowEnabled, isFalse);
// set to page width
{
final table = editorState.getNodeAtPath([0])!;
final beforeWidth = table.width;
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the row menu button
await tester.clickRowMenuButton(0);
// enable header column
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth
.tr(),
),
);
// check the table is updated
expect(table.width, greaterThan(beforeWidth));
}
// distribute columns evenly
{
final table = editorState.getNodeAtPath([0])!;
final beforeWidth = table.width;
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the column menu button
await tester.clickColumnMenuButton(0);
// distribute columns evenly
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys
.document_plugins_simpleTable_moreActions_distributeColumnsWidth
.tr(),
),
);
// check the table is updated
expect(table.width, equals(beforeWidth));
}
});
testWidgets('''
1. insert a simple table via + menu
2. bold
3. clear content
''', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('simple table');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
final firstParagraphPath = [0, 0, 0, 0];
// open the plus menu and select the table block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_table.tr(),
);
// check the block is inserted
final table = editorState.getNodeAtPath([0])!;
expect(table.type, equals(SimpleTableBlockKeys.type));
expect(table.rowLength, equals(2));
expect(table.columnLength, equals(2));
// focus on the first cell
final selection = editorState.selection!;
expect(selection.isCollapsed, isTrue);
expect(selection.start.path, equals(firstParagraphPath));
}
await tester.ime.insertText('Hello');
// enable bold
{
// click the column menu button
await tester.clickColumnMenuButton(0);
// enable bold
await tester.clickSimpleTableBoldContentAction();
await tester.cancelTableActionMenu();
// check the first cell is bold
final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;
expect(paragraph.isInBoldColumn, isTrue);
}
// clear content
{
// focus on the first cell
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: firstParagraphPath)),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
// click the column menu button
await tester.clickColumnMenuButton(0);
final clearContents = find.findTextInFlowyText(
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
.tr(),
);
// clear content
final scrollable = find.descendant(
of: find.byType(SimpleTableBottomSheet),
matching: find.byType(Scrollable),
);
await tester.scrollUntilVisible(
clearContents,
100,
scrollable: scrollable,
);
await tester.tapButton(clearContents);
await tester.cancelTableActionMenu();
// check the first cell is empty
final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;
expect(paragraph.delta!, isEmpty);
}
});
testWidgets('''
1. insert a simple table via + menu
2. insert a heading block in table cell
''', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('simple table');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
final firstParagraphPath = [0, 0, 0, 0];
// open the plus menu and select the table block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.document_slashMenu_name_table.tr(),
);
// check the block is inserted
final table = editorState.getNodeAtPath([0])!;
expect(table.type, equals(SimpleTableBlockKeys.type));
expect(table.rowLength, equals(2));
expect(table.columnLength, equals(2));
// focus on the first cell
final selection = editorState.selection!;
expect(selection.isCollapsed, isTrue);
expect(selection.start.path, equals(firstParagraphPath));
}
// open the plus menu and select the heading block
{
await tester.openPlusMenuAndClickButton(
LocaleKeys.editor_heading1.tr(),
);
// check the heading block is inserted
final heading = editorState.getNodeAtPath([0, 0, 0, 0])!;
expect(heading.type, equals(HeadingBlockKeys.type));
expect(heading.level, equals(1));
}
});
testWidgets('''
1. insert a simple table via + menu
2. resize column
''', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('simple table');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
final beforeWidth = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
// find the first cell
{
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
final offset = tester.getCenter(resizeHandle);
final gesture = await tester.startGesture(offset, pointer: 7);
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(100, 0));
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
}
// check the table is updated
final afterWidth1 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
expect(afterWidth1, greaterThan(beforeWidth));
// resize back to the original width
{
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
final offset = tester.getCenter(resizeHandle);
final gesture = await tester.startGesture(offset, pointer: 7);
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(-100, 0));
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
}
// check the table is updated
final afterWidth2 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
expect(afterWidth2, equals(beforeWidth));
});
});
}
@@ -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() {
@@ -0,0 +1,117 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('toolbar menu:', () {
testWidgets('insert links', (tester) async {
await tester.launchInAnonymousMode();
final createPageButton = find.byKey(
BottomNavigationBarItemType.add.valueKey,
);
await tester.tapButton(createPageButton);
expect(find.byType(MobileDocumentScreen), findsOneWidget);
final editor = find.byType(AppFlowyEditor);
expect(editor, findsOneWidget);
final editorState = tester.editor.getCurrentEditorState();
/// move cursor to content
final root = editorState.document.root;
final lastNode = root.children.lastOrNull;
await editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: lastNode!.path)),
);
await tester.pumpAndSettle();
/// insert two lines of text
const strFirst = 'FirstLine',
strSecond = 'SecondLine',
link = 'google.com';
await editorState.insertTextAtCurrentSelection(strFirst);
await tester.pumpAndSettle();
await editorState.insertNewLine();
await tester.pumpAndSettle();
await editorState.insertTextAtCurrentSelection(strSecond);
await tester.pumpAndSettle();
final firstLine = find.text(strFirst, findRichText: true),
secondLine = find.text(strSecond, findRichText: true);
expect(firstLine, findsOneWidget);
expect(secondLine, findsOneWidget);
/// select the first line
await tester.longPress(firstLine);
await tester.pumpAndSettle();
/// find aa item and tap it
final aaItem = find.byWidgetPredicate(
(widget) =>
widget is AppFlowyMobileToolbarIconItem &&
widget.icon == FlowySvgs.m_toolbar_aa_m,
);
expect(aaItem, findsOneWidget);
await tester.tapButton(aaItem);
/// find link button and tap it
final linkButton = find.byWidgetPredicate(
(widget) =>
widget is MobileToolbarMenuItemWrapper &&
widget.icon == FlowySvgs.m_toolbar_link_m,
);
expect(linkButton, findsOneWidget);
await tester.tapButton(linkButton);
/// input the link
final linkField = find.byWidgetPredicate(
(w) =>
w is FlowyTextField &&
w.hintText == LocaleKeys.document_inlineLink_url_placeholder.tr(),
);
await tester.enterText(linkField, link);
await tester.pumpAndSettle();
/// complete inputting
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
/// do it again
/// select the second line
await tester.longPress(secondLine);
await tester.pumpAndSettle();
await tester.tapButton(aaItem);
await tester.tapButton(linkButton);
await tester.enterText(linkField, link);
await tester.pumpAndSettle();
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
final firstNode = editorState.getNodeAtPath([0]);
final secondNode = editorState.getNodeAtPath([1]);
Map commonDeltaJson(String insert) => {
"insert": insert,
"attributes": {"href": link},
};
expect(
firstNode?.delta?.toJson(),
commonDeltaJson(strFirst),
);
expect(
secondNode?.delta?.toJson(),
commonDeltaJson(strSecond),
);
});
});
}
@@ -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);
});
});
@@ -0,0 +1,81 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Change default text direction', (tester) async {
await tester.launchInAnonymousMode();
/// tap [Setting] button
await tester.tapButton(find.byType(HomePageSettingsPopupMenu));
await tester
.tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr()));
/// tap [Default Text Direction]
await tester.tapButton(
find.text(LocaleKeys.settings_appearance_textDirection_label.tr()),
);
/// there are 3 items: LTR-RTL-AUTO
final bottomSheet = find.ancestor(
of: find.byType(FlowyOptionTile),
matching: find.byType(SafeArea),
);
final items = find.descendant(
of: bottomSheet,
matching: find.byType(FlowyOptionTile),
);
expect(items, findsNWidgets(3));
/// select [Auto]
await tester.tapButton(items.last);
expect(
find.text(LocaleKeys.settings_appearance_textDirection_auto.tr()),
findsOneWidget,
);
/// go back home
await tester.tapButton(find.byType(AppBarImmersiveBackButton));
/// create new page
final createPageButton =
find.byKey(BottomNavigationBarItemType.add.valueKey);
await tester.tapButton(createPageButton);
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
const testEnglish = 'English', testArabic = 'إنجليزي';
/// insert [testEnglish]
await editorState.insertTextAtCurrentSelection(testEnglish);
await tester.pumpAndSettle();
await editorState.insertNewLine(position: editorState.selection!.end);
await tester.pumpAndSettle();
/// insert [testArabic]
await editorState.insertTextAtCurrentSelection(testArabic);
await tester.pumpAndSettle();
final testEnglishFinder = find.text(testEnglish, findRichText: true),
testArabicFinder = find.text(testArabic, findRichText: true);
final testEnglishRenderBox =
testEnglishFinder.evaluate().first.renderObject as RenderBox,
testArabicRenderBox =
testArabicFinder.evaluate().first.renderObject as RenderBox;
final englishPosition = testEnglishRenderBox.localToGlobal(Offset.zero),
arabicPosition = testArabicRenderBox.localToGlobal(Offset.zero);
expect(englishPosition.dx > arabicPosition.dx, true);
});
}
@@ -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,23 @@
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/settings/default_text_direction_test.dart'
as default_text_direction_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();
default_text_direction_test.main();
}
@@ -8,7 +8,8 @@ import 'desktop_runner_5.dart';
import 'desktop_runner_6.dart';
import 'desktop_runner_7.dart';
import 'desktop_runner_8.dart';
import 'mobile_runner.dart';
import 'desktop_runner_9.dart';
import 'mobile_runner_1.dart';
/// The main task runner for all integration tests in AppFlowy.
///
@@ -27,8 +28,9 @@ Future<void> main() async {
await runIntegration6OnDesktop();
await runIntegration7OnDesktop();
await runIntegration8OnDesktop();
await runIntegration9OnDesktop();
} else if (Platform.isIOS || Platform.isAndroid) {
await runIntegrationOnMobile();
await runIntegration1OnMobile();
} else {
throw Exception('Unsupported platform');
}
@@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester {
}
}
Future<void> tapDown(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
PointerDeviceKind kind = PointerDeviceKind.touch,
bool pumpAndSettle = true,
int milliseconds = 500,
}) async {
final location = getCenter(finder);
final TestGesture gesture = await startGesture(
location,
pointer: pointer,
buttons: buttons,
kind: kind,
);
await gesture.cancel();
await gesture.down(location);
await gesture.cancel();
if (pumpAndSettle) {
await this.pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 15),
);
}
}
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,
@@ -4,20 +4,28 @@ 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/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
@@ -42,6 +50,7 @@ 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 'emoji.dart';
import 'util.dart';
@@ -446,11 +455,8 @@ extension CommonOperations on WidgetTester {
// open the page after created
if (openAfterCreated) {
await openPage(
// if the name is null, use the default name
pageName ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
);
// if the name is null, use empty string
await openPage(pageName ?? '', layout: layout);
await pumpAndSettle();
}
}
@@ -595,6 +601,23 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> reorderFavorite({
required String fromName,
required String toName,
}) async {
final from = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.text(fromName),
),
to = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.text(toName),
);
final distanceY = getCenter(to).dy - getCenter(from).dx;
await drag(from, Offset(0, distanceY));
await pumpAndSettle();
}
// tap the button with [FlowySvgData]
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
final button = find.byWidgetPredicate(
@@ -606,9 +629,9 @@ extension CommonOperations on WidgetTester {
// update the page icon in the sidebar
Future<void> updatePageIconInSidebarByName({
required String name,
required String parentName,
String? parentName,
required ViewLayoutPB layout,
required String icon,
required EmojiIconData icon,
}) async {
final iconButton = find.descendant(
of: findPageName(
@@ -620,7 +643,11 @@ extension CommonOperations on WidgetTester {
find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()),
);
await tapButton(iconButton);
await tapEmoji(icon);
if (icon.type == FlowyIconType.emoji) {
await tapEmoji(icon.emoji);
} else if (icon.type == FlowyIconType.icon) {
await tapIcon(icon);
}
await pumpAndSettle();
}
@@ -628,7 +655,7 @@ extension CommonOperations on WidgetTester {
Future<void> updatePageIconInTitleBarByName({
required String name,
required ViewLayoutPB layout,
required String icon,
required EmojiIconData icon,
}) async {
await openPage(
name,
@@ -640,7 +667,11 @@ extension CommonOperations on WidgetTester {
);
await tapButton(title);
await tapButton(find.byType(EmojiPickerButton));
await tapEmoji(icon);
if (icon.type == FlowyIconType.emoji) {
await tapEmoji(icon.emoji);
} else if (icon.type == FlowyIconType.icon) {
await tapIcon(icon);
}
await pumpAndSettle();
}
@@ -787,6 +818,121 @@ extension CommonOperations on WidgetTester {
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);
}
/// Click the column menu button in the simple table
Future<void> clickColumnMenuButton(int index) async {
final columnMenuButton = find.byWidgetPredicate(
(w) =>
w is SimpleTableMobileReorderButton &&
w.index == index &&
w.type == SimpleTableMoreActionType.column,
);
await tapButton(columnMenuButton);
await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));
}
/// Click the row menu button in the simple table
Future<void> clickRowMenuButton(int index) async {
final rowMenuButton = find.byWidgetPredicate(
(w) =>
w is SimpleTableMobileReorderButton &&
w.index == index &&
w.type == SimpleTableMoreActionType.row,
);
await tapButton(rowMenuButton);
await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));
}
/// Click the SimpleTableQuickAction
Future<void> clickSimpleTableQuickAction(SimpleTableMoreAction action) async {
final button = find.byWidgetPredicate(
(widget) => widget is SimpleTableQuickAction && widget.type == action,
);
await tapButton(button);
}
/// Click the SimpleTableContentAction
Future<void> clickSimpleTableBoldContentAction() async {
final button = find.byType(SimpleTableContentBoldAction);
await tapButton(button);
}
/// Cancel the table action menu
Future<void> cancelTableActionMenu() async {
final finder = find.byType(SimpleTableCellBottomSheet);
if (finder.evaluate().isEmpty) {
return;
}
await tapAt(Offset.zero);
await pumpUntilNotFound(finder);
}
/// load icon list and return the first one
Future<EmojiIconData> loadIcon() async {
await loadIconGroups();
final groups = kIconGroups!;
final firstGroup = groups.first;
final firstIcon = firstGroup.icons.first;
return EmojiIconData.icon(
IconsData(
firstGroup.name,
firstIcon.name,
builtInSpaceColors.first,
),
);
}
}
extension SettingsFinder on CommonFinders {
@@ -567,6 +567,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(phantom is PhantomChecklistItem, true);
}
void assertPhantomChecklistItemContent(String content) {
final phantom = find.byType(PhantomChecklistItem);
final text = find.text(content);
expect(find.descendant(of: phantom, matching: text), findsOneWidget);
}
Future<void> openFirstRowDetailPage() async {
await hoverOnFirstRowOfGrid();
@@ -1,3 +1,12 @@
import 'dart:convert';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -11,4 +20,43 @@ extension EmojiTestExtension on WidgetTester {
);
await tapButton(emojiWidget);
}
Future<void> tapIcon(EmojiIconData icon) async {
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
final pickTab = find.byType(PickerTab);
expect(pickTab, findsOneWidget);
await pumpAndSettle();
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.icon.tr),
);
expect(iconTab, findsOneWidget);
await tapButton(iconTab);
final selectedSvg = find.descendant(
of: find.byType(FlowyIconPicker),
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
await tapButton(selectedSvg.first);
final colorPicker = find.byType(IconColorPicker);
expect(colorPicker, findsOneWidget);
final selectedColor = find.descendant(
of: colorPicker,
matching: find.byWidgetPredicate((w) {
if (w is Container) {
final d = w.decoration;
if (d is ShapeDecoration) {
if (d.color ==
Color(int.parse(iconsData.color ?? builtInSpaceColors.first))) {
return true;
}
}
}
return false;
}),
);
await tapButton(selectedColor);
}
}
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
@@ -5,6 +7,8 @@ import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
@@ -14,6 +18,7 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_platform/universal_platform.dart';
@@ -117,7 +122,7 @@ extension Expectation on WidgetTester {
return;
}
final iconWidget = find.byWidgetPredicate(
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
(widget) => widget is EmojiIconWidget && widget.emoji.emoji == emoji,
);
expect(iconWidget, findsOneWidget);
}
@@ -223,24 +228,52 @@ extension Expectation on WidgetTester {
);
}
void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) {
void expectViewHasIcon(String name, ViewLayoutPB layout, EmojiIconData data) {
final pageName = findPageName(
name,
layout: layout,
);
final icon = find.descendant(
of: pageName,
matching: find.text(emoji),
);
expect(icon, findsOneWidget);
final type = data.type;
if (type == FlowyIconType.emoji) {
final icon = find.descendant(
of: pageName,
matching: find.text(data.emoji),
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.icon) {
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
final icon = find.descendant(
of: pageName,
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
expect(icon, findsOneWidget);
}
}
void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) {
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(emoji),
);
expect(icon, findsOneWidget);
void expectViewTitleHasIcon(
String name,
ViewLayoutPB layout,
EmojiIconData data,
) {
final type = data.type;
if (type == FlowyIconType.emoji) {
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(data.emoji),
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.icon) {
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
expect(icon, findsOneWidget);
}
}
void expectSelectedReminder(ReminderOption option) {
@@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy/ai/error.dart';
import 'package:appflowy/ai/openai_client.dart';
import 'package:appflowy/ai/text_completion.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
@@ -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);
Loaded 100 of 3489 files, more files were not shown because too many files have changed in this diff. Show more