Compare commits

...
Author SHA1 Message Date
Richard Shiue ee404369ff fix: row document images (#7322)
* fix: row document images

* fix: calendar

* chore: fallback to documentbloc
2025-02-05 16:42:18 +08:00
Lucas.Xu 58ea654ddf chore: update changelog.md 2025-02-05 16:40:49 +08:00
Lucas.Xu cc14793535 chore: bump version 0.8.3 2025-02-05 14:20:18 +08:00
Nathan.fooo 122fb89c87 chore: fix ai page user profile refresh (#7317) 2025-02-04 20:40:06 +08:00
Lucas e9201cce72 fix: editor stuck on image loading loop when uploading image in row document (#7313)
* fix: editor stuck on image loading loop when uploading image in row document

* test: editor stuck on image loading loop when uploading image in row document
2025-02-04 18:03:28 +08:00
Nathan.fooo 5e5693e249 chore: Support new error code (#7311)
* chore: fetch model list

* chore: suppor new error code
2025-02-03 20:52:31 +08:00
Nathan 5ac0eded3c chore: fetch model list 2025-02-01 23:50:58 +08:00
Lucas dd6b285cd3 chore: update CHANGELOG.md (#7276) 2025-01-23 17:55:12 +08:00
FakhriAzzouz 8b0d5d76a2 New Arabic Translations (#7260)
* 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 🐦
2025-01-23 14:02:32 +08:00
Morn 85b69bde15 chore: cancel button not visible in [Reset to Default] for shortcuts(#7171) (#7257) 2025-01-23 13:58:48 +08:00
Morn dd812d0501 chore: rename [font scale factor] to [scale factor] (#7267) 2025-01-23 13:58:09 +08:00
Richard Shiue a79f825ff1 chore: restrict uploadable image types (#7269)
* chore: restrict image format

* chore: code cleanup
2025-01-23 10:51:15 +08:00
Lucas 862e5629e3 fix: v0.8.2 launch review issues (#7266)
* fix: unable to copy table from Apple Notes

* fix: disable pasting a table into another table

* fix: ignore the delete shortcut if the next node is a simple table node
2025-01-22 22:46:03 +08:00
Richard Shiue c5a91e10df chore: support pasting image links that have no file extension (#7262)
* test: speed up copypasta tests

* chore: strip away markdown syntax if message is image only

* chore: paste image urls with no file extension

* test: add integration test

* test: group tests

* chore: apply code suggestions to 3 files
2025-01-22 22:13:18 +08:00
Lucas 0b0e10baa8 feat: turn -> into to → in document (#7256)
* chore: bump version 0.8.2

* feat: turn -> into to → in document

* Revert "chore: bump version 0.8.2"

This reverts commit 45efd4d7d7.

* test: add shortcut tests
2025-01-22 15:05:22 +08:00
Nathan ec18a3c443 chore: update docs 2025-01-22 09:47:06 +08:00
Nathan.fooo a0867ed688 chore: return ai image error (#7263) 2025-01-22 09:42:24 +08:00
Nathan.fooo cdf8e68ff2 Update plan desc (#7258)
* chore: update plan desc

* chore: update plan desc
2025-01-22 02:02:30 +08:00
Richard Shiue af92303e38 fix: launch review issues 0.8.2 (#7261) 2025-01-21 22:17:53 +08:00
Lucas 504041d75a chore: bump version 0.8.2 (#7255) 2025-01-21 13:31:40 +08:00
Lucas 06ab965413 chore: only enable document integrity check when enableDocumentInternalLog is on (#7251)
* chore: only enable document integrity check when enableDocumentInternalLog is on

* feat: copy divider and table from ChatGPT

* test: add copy from ChatGPT test

* feat: support copying link from keyboard clipboard
2025-01-21 13:29:31 +08:00
Morn 42bd4884fd feat: support custom image icon (#7236)
* feat: supporting upload custom image as icon

* feat: support custom image icon on mobile

* chore: clean code
2025-01-21 11:58:45 +08:00
Lucas 0c057243bc chore: bump client api (#7249)
* chore: bump client api

* chore: set default publish info to true
2025-01-21 09:18:25 +08:00
Lucas 8fdc6e9638 feat: support pasting table from notion, google docs and google sheet (#7247)
* feat: support pasting table from Notion

* test: add google docs / googles sheets table test

* fix: google docs test

* fix: paste table from notion test
2025-01-21 09:12:57 +08:00
Morn 187f7409ce feat: enable customizing icons for database views (#7187)
* feat: enable set icon for databse tab

* chore: resolve conflicts

* feat: enable changing dabtabse icon on mobile
2025-01-20 17:55:01 +08:00
Morn e3ce6e8b4b fix: use WindowSizeManager to zoom on mobile (#7215) 2025-01-20 17:54:26 +08:00
Richard Shiue cfe481759f chore: bump table column width when saving chat message to page (#7243)
* chore: bump table column width when saving chat message to page

* test: add unit test
2025-01-20 13:28:02 +08:00
Richard Shiue 6bcef33d05 chore: trim markdown text (#7242)
* chore: trim markdown text

* chore: trim before saving
2025-01-20 11:47:03 +08:00
Lucas aa8c9bad9f fix: remove app from homepage url (#7233)
* fix: remove app from namespace url

* fix: banner issue on mobile

* Revert "fix: remove app from namespace url"

This reverts commit 4bd5f3f590.

* fix: remove app from namespace url
2025-01-17 20:55:22 +08:00
Richard Shiue 1723886f3a feat: save messages as a new page (#7224) 2025-01-17 20:38:55 +08:00
FakhriAzzouz 63c7f7b6fe chore: update ar-SA translations (#7216)
* 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 🐦
2025-01-17 10:08:09 +08:00
Lucas f35dfaf525 fix: improve the document diff function to prevent partial ordering issues (#7217)
* fix: improve the document diff function to prevent partial ordering issues

* fix: improve the document diff function to prevent partial ordering issues

* fix: nested block padding issues

* fix: improve the document diff function to prevent partial ordering issues

* chore: update editor version

* test: add no diff test and update text diff test

* test: delete and insert text diff with different id

* test: insert single text / delete single text tests

* test: multiple delete and update diff

* test: multiple insert and update diff

* chore: revert cargo changes

* chore: remove unused code

* chore: optimize the code logic
2025-01-16 21:01:23 +08:00
Richard Shiue eead2d20f5 fix: pass alwaysDistributeColumnWidths to simple table block (#7228) 2025-01-16 16:42:24 +08:00
Richard Shiue f73c2540c6 chore: reorganize code (#7206) 2025-01-15 19:57:47 +08:00
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
3331 changed files with 39234 additions and 145936 deletions

No files matched your search

+2 -3
View File
@@ -7,7 +7,6 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
pull_request:
branches:
@@ -20,7 +19,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.80.1"
RUST_TOOLCHAIN: "1.81.0"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64
@@ -152,7 +151,7 @@ jobs:
rustup target install aarch64-linux-android
rustup target install x86_64-linux-android
rustup target add armv7-linux-androideabi
cargo install --force duckscript_cli
cargo install --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
+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, 9]
os: [ ubuntu-latest ]
test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
+2 -4
View File
@@ -7,7 +7,6 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
pull_request:
@@ -16,12 +15,11 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
env:
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.80.1"
RUST_TOOLCHAIN: "1.81.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -85,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
+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
+2 -72
View File
@@ -18,81 +18,11 @@ on:
env:
CARGO_TERM_COLOR: always
CLOUD_VERSION: 0.7.6-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
sed -i '' 's|APPFLOWY_AI_OPENAI_API_KEY=.*|APPFLOWY_AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}|' .env
- name: Ensure AppFlowy-Cloud is Running with Correct Version
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: ${{ 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
+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
+107 -1
View File
@@ -1,4 +1,110 @@
# Release Notes
## Version 0.8.3 - 05/02/2025
### New Features
- Support OpenAI o3-mini model
### Bug Fixes
- Fixed an issue where users were unable to upload images in row pages
- Fixed an issue where users were unable to upload files on Android devices
## Version 0.8.2 - 23/01/2025
### New Features
- Customized database view icons
- Support for uploading images as custom icons
- Enabled selecting multiple AI messages to save into a document
- Added the ability to scale the app's display size on mobile
- Support for pasting image links without file extensions
### Bug Fixes
- Fixed an issue where pasting tables from other apps wasn't working
- Fixed homepage URL issues in Settings
- Fixed an issue where the 'Cancel' button was not visible on the Shortcuts page
## 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
@@ -939,4 +1045,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
+1 -1
View File
@@ -20,7 +20,7 @@ workflows:
rustup target install aarch64-apple-ios-sim
cargo install --force cargo-make
cargo install --force duckscript_cli
cargo install --force --locked duckscript_cli
cargo install --force cargo-lipo
cargo make appflowy-flutter-deps-tools
+123 -138
View File
@@ -1,140 +1,125 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// This task only builds the Dart code of AppFlowy.
// It supports both the desktop and mobile version.
"name": "AF: Build Dart Only",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"env": {
"RUST_LOG": "debug",
},
// uncomment the following line to testing performance.
// "flutterMode": "profile",
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core",
"env": {
"RUST_LOG": "trace",
"RUST_BACKTRACE": "1"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds will:
// - call the clean task,
// - rebuild all the generated Files (including freeze and language files)
// - rebuild the the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For Android",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-desktop: Debug Rust",
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}"
// To launch the application directly, use the following configuration:
// "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/"
},
]
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// This task only builds the Dart code of AppFlowy.
// It supports both the desktop and mobile version.
"name": "AF: Build Dart Only",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"env": {
"RUST_LOG": "debug",
},
// uncomment the following line to testing performance.
// "flutterMode": "profile",
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core",
"env": {
"RUST_LOG": "trace",
"RUST_BACKTRACE": "1"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds will:
// - call the clean task,
// - rebuild all the generated Files (including freeze and language files)
// - rebuild the the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For Android",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-desktop: Debug Rust",
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}"
// To launch the application directly, use the following configuration:
// "request": "launch",
// "program": "[YOUR_APPLICATION_PATH]",
},
]
}
-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.6"
APPFLOWY_VERSION = "0.8.3"
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>
@@ -1,16 +1,21 @@
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'database/database_test_runner.dart' as database_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 +27,9 @@ Future<void> main() async {
// sidebar
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
// database
database_test_runner.main();
}
@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide UploadImageMenu, ResizableImage;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../../shared/constants.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// copy link to block
group('database image:', () {
testWidgets('insert image', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open the first row detail page and upload an image
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Grid,
pageName: 'database image',
);
await tester.openFirstRowDetailPage();
// insert an image block
{
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
}
// upload an image
{
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');
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
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], isNotEmpty);
// remove the temp file
file.deleteSync();
}
});
});
}
@@ -0,0 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'database_image_test.dart' as database_image_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_image_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);
});
}
@@ -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,10 +1,11 @@
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/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:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -102,8 +103,7 @@ void main() {
expect(memberCount, findsNWidgets(2));
});
testWidgets('only display one menu item in the workspace menu',
(tester) async {
testWidgets('workspace menu popover behavior test', (tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
@@ -128,6 +128,8 @@ void main() {
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 {
@@ -136,15 +138,73 @@ void main() {
);
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,
);
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);
},
);
});
@@ -54,7 +54,7 @@ void main() {
);
final shareValues = plainText!
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, isNotEmpty);
@@ -1,5 +1,6 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.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';
@@ -71,5 +72,16 @@ void main() {
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);
});
});
}
@@ -120,6 +120,10 @@ void main() {
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
if (pageItem.evaluate().isEmpty) {
return;
}
expect(pageItem, findsOneWidget);
// comment it out because it's not allowed to update the namespace in free plan
@@ -249,7 +253,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(
@@ -257,6 +261,10 @@ More actions for published page:
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
if (pageItem.evaluate().isEmpty) {
return;
}
expect(pageItem, findsOneWidget);
final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());
@@ -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();
@@ -0,0 +1,190 @@
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.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/shared/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.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/emoji.dart';
import '../../shared/util.dart';
void main() {
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
testWidgets('change icon', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
const pageName = 'Database';
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Grid,
name: pageName,
);
/// create board
final addButton = find.byType(AddDatabaseViewButton);
await tester.tapButton(addButton);
await tester.tapButton(
find.text(
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Board.layoutName}',
findRichText: true,
),
);
/// create calendar
await tester.tapButton(addButton);
await tester.tapButton(
find.text(
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Calendar.layoutName}',
findRichText: true,
),
);
final databaseTabBarItem = find.byType(DatabaseTabBarItem);
expect(databaseTabBarItem, findsNWidgets(3));
final gridItem = databaseTabBarItem.first,
boardItem = databaseTabBarItem.at(1),
calendarItem = databaseTabBarItem.last;
/// change the icon of grid
/// the first tapping is to select specific item
/// the second tapping is to show the menu
await tester.tapButton(gridItem);
await tester.tapButton(gridItem);
/// change icon
await tester
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
await tester.tapIcon(iconData, enableColor: false);
final gridIcon = find.descendant(
of: gridItem,
matching: find.byType(RawEmojiIconWidget),
);
final gridIconWidget =
gridIcon.evaluate().first.widget as RawEmojiIconWidget;
final iconsData = IconsData.fromJson(jsonDecode(iconData.emoji));
final gridIconsData =
IconsData.fromJson(jsonDecode(gridIconWidget.emoji.emoji));
expect(gridIconsData.iconName, iconsData.iconName);
/// change the icon of board
await tester.tapButton(boardItem);
await tester.tapButton(boardItem);
await tester
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
await tester.tapIcon(iconData, enableColor: false);
final boardIcon = find.descendant(
of: boardItem,
matching: find.byType(RawEmojiIconWidget),
);
final boardIconWidget =
boardIcon.evaluate().first.widget as RawEmojiIconWidget;
final boardIconsData =
IconsData.fromJson(jsonDecode(boardIconWidget.emoji.emoji));
expect(boardIconsData.iconName, iconsData.iconName);
/// change the icon of calendar
await tester.tapButton(calendarItem);
await tester.tapButton(calendarItem);
await tester
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
await tester.tapIcon(iconData, enableColor: false);
final calendarIcon = find.descendant(
of: calendarItem,
matching: find.byType(RawEmojiIconWidget),
);
final calendarIconWidget =
calendarIcon.evaluate().first.widget as RawEmojiIconWidget;
final calendarIconsData =
IconsData.fromJson(jsonDecode(calendarIconWidget.emoji.emoji));
expect(calendarIconsData.iconName, iconsData.iconName);
});
testWidgets('change database icon from sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
final icon = IconsData.fromJson(jsonDecode(iconData.emoji)), emoji = '😄';
const pageName = 'Database';
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Grid,
name: pageName,
);
final viewItem = find.descendant(
of: find.byType(SidebarFolder),
matching: find.byWidgetPredicate(
(w) => w is ViewItem && w.view.name == pageName,
),
);
/// change icon to emoji
await tester.tapButton(
find.descendant(
of: viewItem,
matching: find.byType(FlowySvg),
),
);
await tester.tapEmoji(emoji);
final iconWidget = find.descendant(
of: viewItem,
matching: find.byType(RawEmojiIconWidget),
);
expect(
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
emoji,
);
/// the icon will not be displayed in database item
Finder databaseIcon = find.descendant(
of: find.byType(DatabaseTabBarItem),
matching: find.byType(FlowySvg),
);
expect(
(databaseIcon.evaluate().first.widget as FlowySvg).svg,
FlowySvgs.icon_grid_s,
);
/// change emoji to icon
await tester.tapButton(iconWidget);
await tester.tapIcon(iconData);
expect(
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
iconData.emoji,
);
databaseIcon = find.descendant(
of: find.byType(DatabaseTabBarItem),
matching: find.byType(RawEmojiIconWidget),
);
final databaseIconWidget =
databaseIcon.evaluate().first.widget as RawEmojiIconWidget;
final databaseIconsData =
IconsData.fromJson(jsonDecode(databaseIconWidget.emoji.emoji));
expect(icon.svgString, databaseIconsData.svgString);
expect(icon.color, isNotEmpty);
expect(icon.color, databaseIconsData.color);
/// the icon in database item should not show the color
expect(databaseIconWidget.enableColor, false);
});
}
@@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_edi
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';
@@ -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 {
@@ -386,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,
@@ -398,6 +411,7 @@ void main() {
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 2);
tester.assertPhantomChecklistItemContent("");
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
@@ -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';
@@ -20,17 +19,20 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('copy and paste in document', () {
group('copy and paste in document:', () {
testWidgets('paste multiple lines at the first line', (tester) async {
// mock the clipboard
const lines = 3;
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',
);
}
@@ -170,301 +172,349 @@ void main() {
},
);
});
});
testWidgets('paste text on part of bullet list', (tester) async {
const plainText = 'test';
testWidgets('paste text on part of bullet list', (tester) async {
const plainText = 'test';
await tester.pasteContent(
plainText: plainText,
beforeTest: (editorState) async {
final transaction = editorState.transaction;
transaction.insertNodes(
[0],
[
Node(
type: BulletedListBlockKeys.type,
attributes: {
'delta': [
{"insert": "bullet list"},
],
},
),
],
);
// Set the selection to the second numbered list node (which has empty delta)
transaction.afterSelection = Selection(
start: Position(path: [0], offset: 7),
end: Position(path: [0], offset: 11),
);
await editorState.apply(transaction);
await tester.pumpAndSettle();
},
(editorState) {
final node = editorState.getNodeAtPath([0]);
expect(node?.delta?.toPlainText(), 'bullet test');
expect(node?.type, BulletedListBlockKeys.type);
},
);
});
testWidgets('paste image(png) from memory', (tester) async {
final image = await rootBundle.load('assets/test/images/sample.png');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('png', bytes), (editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
});
testWidgets('paste image(jpeg) from memory', (tester) async {
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
});
testWidgets('paste image(gif) from memory', (tester) async {
final image = await rootBundle.load('assets/test/images/sample.gif');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('gif', bytes), (editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
});
testWidgets(
'format the selected text to href when pasting url if available',
(tester) async {
const text = 'appflowy';
const url = 'https://appflowy.io';
await tester.pasteContent(
plainText: url,
plainText: plainText,
beforeTest: (editorState) async {
await tester.ime.insertText(text);
await tester.editor.updateSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
final transaction = editorState.transaction;
transaction.insertNodes(
[0],
[
Node(
type: BulletedListBlockKeys.type,
attributes: {
'delta': [
{"insert": "bullet list"},
],
},
),
],
);
},
(editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{
'insert': text,
'attributes': {'href': url},
}
]);
},
);
},
);
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
testWidgets(
'paste the image from clipboard when html and image are both available',
(tester) async {
const html =
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
final image = await rootBundle.load('assets/test/images/sample.png');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(
html: html,
image: ('png', bytes),
(editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
},
);
},
);
// Set the selection to the second numbered list node (which has empty delta)
transaction.afterSelection = Selection(
start: Position(path: [0], offset: 7),
end: Position(path: [0], offset: 11),
);
testWidgets('paste the html content contains section', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 2);
final node1 = editorState.getNodeAtPath([0])!;
final node2 = editorState.getNodeAtPath([1])!;
expect(node1.type, ParagraphBlockKeys.type);
expect(node2.type, ParagraphBlockKeys.type);
});
});
testWidgets('paste the html from google translation', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 8);
});
});
testWidgets(
'auto convert url to link preview block',
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) async {
// the second one is the paragraph node
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
// hover on the link preview block
// click the more button
// and select convert to link
await tester.hoverOnWidget(
find.byType(CustomLinkPreviewWidget),
onHover: () async {
final convertToLinkButton = find.byWidgetPredicate((widget) {
return widget is MenuBlockButton &&
widget.tooltip ==
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
});
expect(convertToLinkButton, findsOneWidget);
await tester.tap(convertToLinkButton);
await editorState.apply(transaction);
await tester.pumpAndSettle();
},
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final textNode = editorState.getNodeAtPath([0])!;
expect(textNode.type, ParagraphBlockKeys.type);
expect(textNode.delta!.toJson(), [
{
'insert': url,
'attributes': {'href': url},
}
]);
},
);
testWidgets(
'ctrl/cmd+z to undo the auto convert url to link preview block',
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) async {
// the second one is the paragraph node
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
await tester.editor.tapLineOfEditorAt(0);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{
'insert': url,
'attributes': {'href': url},
}
]);
},
);
testWidgets(
'paste the nodes start with non-delta node',
(tester) async {
await tester.pasteContent((_) {});
const text = 'Hello World';
final editorState = tester.editor.getCurrentEditorState();
final transaction = editorState.transaction;
// [image_block]
// [paragraph_block]
transaction.insertNodes([
0,
], [
customImageNode(url: ''),
paragraphNode(text: text),
]);
await editorState.apply(transaction);
await tester.pumpAndSettle();
await tester.editor.tapLineOfEditorAt(0);
// select all and copy
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
// put the cursor to the end of the paragraph block
await tester.editor.tapLineOfEditorAt(0);
// paste the content
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
// expect the image and the paragraph block are inserted below the cursor
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
},
);
testWidgets('paste the url without protocol', (tester) async {
// paste the image that from local file
const plainText = '1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
(editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
final node = editorState.getNodeAtPath([0]);
expect(node?.delta?.toPlainText(), 'bullet test');
expect(node?.type, BulletedListBlockKeys.type);
},
);
});
});
testWidgets('paste the image url', (tester) async {
const plainText = 'https://appflowy.io/1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
testWidgets('paste image(png) from memory', (tester) async {
final image = await rootBundle.load('assets/test/images/sample.png');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('png', bytes), (editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
});
testWidgets('paste image(jpeg) from memory', (tester) async {
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
});
testWidgets('paste image(gif) from memory', (tester) async {
final image = await rootBundle.load('assets/test/images/sample.gif');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(image: ('gif', bytes), (editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
});
testWidgets(
'format the selected text to href when pasting url if available',
(tester) async {
const text = 'appflowy';
const url = 'https://appflowy.io';
await tester.pasteContent(
plainText: url,
beforeTest: (editorState) async {
await tester.ime.insertText(text);
await tester.editor.updateSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
);
},
(editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{
'insert': text,
'attributes': {'href': url},
}
]);
},
);
},
);
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
testWidgets(
'paste the image from clipboard when html and image are both available',
(tester) async {
const html =
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
final image = await rootBundle.load('assets/test/images/sample.png');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(
html: html,
image: ('png', bytes),
(editorState) {
expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
},
);
},
);
testWidgets('paste the html content contains section', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 2);
final node1 = editorState.getNodeAtPath([0])!;
final node2 = editorState.getNodeAtPath([1])!;
expect(node1.type, ParagraphBlockKeys.type);
expect(node2.type, ParagraphBlockKeys.type);
});
});
testWidgets('paste the html from google translation', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: bordeLine truncated
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 8);
});
});
testWidgets(
'auto convert url to link preview block',
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) async {
// the second one is the paragraph node
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
// hover on the link preview block
// click the more button
// and select convert to link
await tester.hoverOnWidget(
find.byType(CustomLinkPreviewWidget),
onHover: () async {
final convertToLinkButton = find.byWidgetPredicate((widget) {
return widget is MenuBlockButton &&
widget.tooltip ==
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
});
expect(convertToLinkButton, findsOneWidget);
await tester.tap(convertToLinkButton);
await tester.pumpAndSettle();
},
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final textNode = editorState.getNodeAtPath([0])!;
expect(textNode.type, ParagraphBlockKeys.type);
expect(textNode.delta!.toJson(), [
{
'insert': url,
'attributes': {'href': url},
}
]);
},
);
testWidgets(
'ctrl/cmd+z to undo the auto convert url to link preview block',
(tester) async {
const url = 'https://appflowy.io';
await tester.pasteContent(plainText: url, (editorState) async {
// the second one is the paragraph node
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
await tester.editor.tapLineOfEditorAt(0);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{
'insert': url,
'attributes': {'href': url},
}
]);
},
);
testWidgets(
'paste the nodes start with non-delta node',
(tester) async {
await tester.pasteContent((_) {});
const text = 'Hello World';
final editorState = tester.editor.getCurrentEditorState();
final transaction = editorState.transaction;
// [image_block]
// [paragraph_block]
transaction.insertNodes([
0,
], [
customImageNode(url: ''),
paragraphNode(text: text),
]);
await editorState.apply(transaction);
await tester.pumpAndSettle();
await tester.editor.tapLineOfEditorAt(0);
// select all and copy
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
// put the cursor to the end of the paragraph block
await tester.editor.tapLineOfEditorAt(0);
// paste the content
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed:
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
isMetaPressed: UniversalPlatform.isMacOS,
);
await tester.pumpAndSettle();
// expect the image and the paragraph block are inserted below the cursor
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
},
);
testWidgets('paste the url without protocol', (tester) async {
// paste the image that from local file
const plainText = '1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
(editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});
testWidgets('paste the image url', (tester) async {
const plainText = 'https://appflowy.io/1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
(editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});
testWidgets('paste image url without extension', (tester) async {
const plainText =
'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.pasteContent(plainText: plainText, (editorState) {
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
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 node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
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);
}
},
);
});
});
}
@@ -476,13 +526,14 @@ extension on WidgetTester {
String? plainText,
String? html,
String? inAppJson,
bool pasteAsPlain = false,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
await tapAnonymousSignInButton();
// create a new document
await createNewPageWithNameUnderParent(name: 'Test Document');
await createNewPageWithNameUnderParent();
// tap the editor
await tapButton(find.byType(AppFlowyEditor));
@@ -502,9 +553,10 @@ extension on WidgetTester {
await simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isShiftPressed: pasteAsPlain,
isMetaPressed: Platform.isMacOS,
);
await pumpAndSettle();
await pumpAndSettle(const Duration(milliseconds: 1000));
test(editor.getCurrentEditorState());
}
@@ -37,12 +37,12 @@ void main() {
// set clipboard data
final data = [
"123456\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"1234567\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"12345678\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"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(
@@ -139,6 +139,22 @@ void main() {
),
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,7 +1,9 @@
import 'dart:io';
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';
@@ -11,6 +13,7 @@ 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:
@@ -37,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',
@@ -48,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),
@@ -67,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));
@@ -91,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));
@@ -144,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));
@@ -202,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));
@@ -243,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));
@@ -293,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));
@@ -336,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));
@@ -384,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);
@@ -411,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));
@@ -437,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]);
@@ -498,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,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,87 +7,25 @@ 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';
const _testImageUrls = [
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640',
'https://www.easygifanimator.net/images/samples/eglite.gif',
'https://people.math.sc.edu/Burkardt/data/bmp/snail.bmp',
'https://file-examples.com/storage/fe9566cb7d67345489a5a97/2017/10/file_example_JPG_100kB.jpg',
];
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
group('image block in document', () {
Future<void> testEmbedImage(WidgetTester tester, String url) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
);
await tester.enterText(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField),
),
url,
);
await tester.tapButton(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.text(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
findRichText: true,
),
),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], url);
}
testWidgets('insert an image from local file', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -138,42 +76,6 @@ void main() {
file.deleteSync();
});
for (final url in _testImageUrls) {
testWidgets('insert an image from network: $url', (tester) async {
await testEmbedImage(tester, url);
});
}
testWidgets('insert an image from unsplash', (tester) async {
await runWithNetworkImages(() async {
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);
});
});
}
@@ -1,5 +1,4 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -115,19 +114,41 @@ void main() {
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// hover on the table
final tableBlock = find.byType(SimpleTableBlockWidget).first;
// add a new row
final row = find.byWidgetPredicate((w) {
return w is SimpleTableRowBlockWidget && w.node.rowIndex == 1;
});
await tester.hoverOnWidget(
tableBlock,
row,
onHover: () async {
// click the add row button
final addRowButton = find.byType(SimpleTableAddRowButton).first;
await tester.tap(addRowButton);
},
);
await tester.pumpAndSettle();
// click the add column button
// 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;
@@ -311,6 +332,398 @@ void main() {
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 {
@@ -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,262 @@
import 'dart:io';
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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/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,
);
}
});
testWidgets('Update page custom icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local 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());
final iconData = EmojiIconData.custom(imageFile.path);
// 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,11 +1,15 @@
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';
@@ -285,6 +289,39 @@ void main() {
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);
});
});
}
@@ -1,7 +1,10 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/database/database_icon_test.dart' as database_icon_test;
import 'desktop/first_test/first_test.dart' as first_test;
import 'desktop/uncategorized/code_block_language_selector_test.dart'
as code_language_selector;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
Future<void> main() async {
await runIntegration9OnDesktop();
@@ -11,6 +14,7 @@ Future<void> runIntegration9OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
tabs_test.main();
code_language_selector.main();
database_icon_test.main();
}
@@ -2,7 +2,9 @@ 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();
@@ -11,4 +13,6 @@ void main() {
title_test.main();
page_style_test.main();
plus_menu_test.main();
simple_table_test.main();
toolbar_test.main();
}
@@ -0,0 +1,65 @@
import 'dart:io';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document title:', () {
testWidgets('update page custom icon in title bar', (tester) async {
await tester.launchInAnonymousMode();
/// prepare local 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());
final iconData = EmojiIconData.custom(imageFile.path);
/// create an empty page
await tester
.tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));
/// show Page style page
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
final pageStyleIcon = find.byType(PageStyleIcon);
final iconInPageStyleIcon = find.descendant(
of: pageStyleIcon,
matching: find.byType(RawEmojiIconWidget),
);
expect(iconInPageStyleIcon, findsNothing);
/// show icon picker
await tester.tapButton(pageStyleIcon);
/// upload custom icon
await tester.pickImage(iconData);
/// check result
final documentPage = find.byType(MobileDocumentScreen);
final rawEmojiIconWidget = find
.descendant(
of: documentPage,
matching: find.byType(RawEmojiIconWidget),
)
.evaluate()
.first
.widget as RawEmojiIconWidget;
final iconDataInWidget = rawEmojiIconWidget.emoji;
expect(iconDataInWidget.type, FlowyIconType.custom);
});
});
}
@@ -1,17 +1,30 @@
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/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.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/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:flutter/material.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() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('document page style:', () {
double getCurrentEditorFontSize() {
@@ -114,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,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));
});
});
}
@@ -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),
);
});
});
}
@@ -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);
});
}
@@ -0,0 +1,48 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.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();
testWidgets('test for change scale factor', (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 [Font Scale Factor]
await tester.tapButton(
find.text(LocaleKeys.settings_appearance_fontScaleFactor.tr()),
);
/// drag slider
final slider = find.descendant(
of: find.byType(FontSizeStepper),
matching: find.byType(Slider),
);
await tester.slideToValue(slider, 0.8);
expect(appflowyScaleFactor, 0.8);
await tester.slideToValue(slider, 0.9);
expect(appflowyScaleFactor, 0.9);
await tester.slideToValue(slider, 1.0);
expect(appflowyScaleFactor, 1.0);
await tester.slideToValue(slider, 1.1);
expect(appflowyScaleFactor, 1.1);
await tester.slideToValue(slider, 1.2);
expect(appflowyScaleFactor, 1.2);
});
}
@@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
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> main() async {
@@ -17,4 +19,5 @@ Future<void> runIntegration1OnMobile() async {
anonymous_sign_in_test.main();
create_new_page_test.main();
document_test_runner.main();
default_text_direction_test.main();
}
@@ -13,6 +13,7 @@ import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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:path/path.dart' as p;
@@ -175,6 +176,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,
@@ -208,6 +236,25 @@ extension AppFlowyTestBase on WidgetTester {
Future<void> wait(int milliseconds) async {
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> slideToValue(
Finder slider,
double value, {
double paddingOffset = 24.0,
}) async {
final sliderWidget = slider.evaluate().first.widget as Slider;
final range = sliderWidget.max - sliderWidget.min;
final initialRate = (value - sliderWidget.min) / range;
final totalWidth = getSize(slider).width - (2 * paddingOffset);
final zeroPoint = getTopLeft(slider) +
Offset(
paddingOffset + initialRate * totalWidth,
getSize(slider).height / 2,
);
final calculatedOffset = value * (totalWidth / 100);
await dragFrom(zeroPoint, Offset(calculatedOffset, 0));
await pumpAndSettle();
}
}
extension AppFlowyFinderTestBase on CommonFinders {
@@ -8,18 +8,24 @@ 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';
@@ -449,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();
}
}
@@ -598,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(
@@ -609,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(
@@ -623,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();
}
@@ -631,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,
@@ -643,7 +667,13 @@ 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);
} else if (icon.type == FlowyIconType.custom) {
await pickImage(icon);
}
await pumpAndSettle();
}
@@ -841,6 +871,70 @@ extension CommonOperations on WidgetTester {
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,16 @@
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/icon_uploader.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:cross_file/cross_file.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flowy_infra_ui/style_widget/primary_rounded_button.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 +24,84 @@ extension EmojiTestExtension on WidgetTester {
);
await tapButton(emojiWidget);
}
Future<void> tapIcon(EmojiIconData icon, {bool enableColor = true}) 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);
if (enableColor) {
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);
}
}
Future<void> pickImage(EmojiIconData icon) async {
final pickTab = find.byType(PickerTab);
expect(pickTab, findsOneWidget);
await pumpAndSettle();
/// switch to custom tab
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.custom.tr),
);
expect(iconTab, findsOneWidget);
await tapButton(iconTab);
/// mock for dragging image
final dropTarget = find.descendant(
of: find.byType(IconUploader),
matching: find.byType(DropTarget),
);
expect(dropTarget, findsOneWidget);
final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget;
dropTargetWidget.onDragDone?.call(
DropDoneDetails(
files: [XFile(icon.emoji)],
localPosition: Offset.zero,
globalPosition: Offset.zero,
),
);
await pumpAndSettle(const Duration(seconds: 3));
/// confirm to upload
final confirmButton = find.descendant(
of: find.byType(IconUploader),
matching: find.byType(PrimaryRoundedButton),
);
await tapButton(confirmButton);
}
}
@@ -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,9 @@ 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/appflowy_network_image.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,8 +19,10 @@ 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:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
import 'util.dart';
@@ -117,7 +124,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 +230,80 @@ 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);
} else if (type == FlowyIconType.custom) {
if (isURL(data.emoji)) {
final image = find.descendant(
of: pageName,
matching: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: pageName,
matching: find.byType(Image),
);
expect(image, 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);
} else if (type == FlowyIconType.custom) {
if (isURL(data.emoji)) {
final image = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byType(Image),
);
expect(image, 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/service/error.dart';
import 'package:appflowy/ai/service/openai_client.dart';
import 'package:appflowy/ai/service/text_completion.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
+28 -22
View File
@@ -88,6 +88,8 @@ PODS:
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@@ -112,6 +114,7 @@ DEPENDENCIES:
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS:
trunk:
@@ -168,38 +171,41 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2
@@ -8,5 +8,12 @@
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:appflowy.com</string>
<string>applinks:appflowy.io</string>
<string>applinks:test.appflowy.com</string>
<string>applinks:test.appflowy.io</string>
</array>
</dict>
</plist>
+12
View File
@@ -0,0 +1,12 @@
export 'widgets/loading_indicator.dart';
export 'widgets/prompt_input/action_buttons.dart';
export 'widgets/prompt_input/desktop_input_text_field.dart';
export 'widgets/prompt_input/file_attachment_list.dart';
export 'widgets/prompt_input/layout_define.dart';
export 'widgets/prompt_input/mention_page_bottom_sheet.dart';
export 'widgets/prompt_input/mention_page_menu.dart';
export 'widgets/prompt_input/mentioned_page_text_span.dart';
export 'widgets/prompt_input/predefined_format_buttons.dart';
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
export 'widgets/prompt_input/select_sources_menu.dart';
export 'widgets/prompt_input/send_button.dart';
@@ -1,8 +1,9 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'error.dart';
import 'text_completion.dart';
abstract class AIRepository {
Future<void> getStreamedCompletions({
required String prompt,
@@ -17,6 +18,7 @@ abstract class AIRepository {
});
Future<void> streamCompletion({
String? objectId,
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
@@ -3,16 +3,51 @@ import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ask_ai_action.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart' as fixnum;
import 'ai_client.dart';
import 'error.dart';
import 'text_completion.dart';
enum AskAIAction {
summarize,
fixSpelling,
improveWriting,
makeItLonger;
String get toInstruction => switch (this) {
summarize => 'Tl;dr',
fixSpelling => 'Correct this to standard English:',
improveWriting => 'Rewrite this in your own words:',
makeItLonger => 'Make this text longer:',
};
String prompt(String input) => switch (this) {
summarize => '$input\n\n$toInstruction',
_ => "$toInstruction\n\n$input",
};
static AskAIAction from(int index) => switch (index) {
0 => summarize,
1 => fixSpelling,
2 => improveWriting,
3 => makeItLonger,
_ => fixSpelling
};
String get name => switch (this) {
summarize => LocaleKeys.document_plugins_smartEditSummarize.tr(),
fixSpelling => LocaleKeys.document_plugins_smartEditFixSpelling.tr(),
improveWriting =>
LocaleKeys.document_plugins_smartEditImproveWriting.tr(),
makeItLonger => LocaleKeys.document_plugins_smartEditMakeLonger.tr(),
};
}
class AppFlowyAIService implements AIRepository {
@override
Future<FlowyResult<List<String>, AIError>> generateImage({
@@ -39,6 +74,7 @@ class AppFlowyAIService implements AIRepository {
@override
Future<CompletionStream> streamCompletion({
String? objectId,
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
@@ -52,10 +88,17 @@ class AppFlowyAIService implements AIRepository {
onEnd,
onError,
);
final List<String> ragIds = [];
if (objectId != null) {
ragIds.add(objectId);
}
final payload = CompleteTextPB(
text: text,
completionType: completionType,
streamPort: fixnum.Int64(stream.nativePort),
objectId: objectId ?? "",
ragIds: ragIds,
);
// ignore: unawaited_futures
@@ -96,6 +139,24 @@ class CompletionStream {
);
}
if (event == "AI_IMAGE_RESPONSE_LIMIT") {
onError(
AIError(
message: LocaleKeys.sideBar_aiImageResponseLimit.tr(),
code: AIErrorCode.aiImageResponseLimitExceeded,
),
);
}
if (event.startsWith("AI_MAX_REQUIRED:")) {
final msg = event.substring(16);
onError(
AIError(
message: msg,
),
);
}
if (event.startsWith("start:")) {
await onStart();
}
@@ -17,6 +17,8 @@ class AIError with _$AIError {
enum AIErrorCode {
@JsonValue('AIResponseLimitExceeded')
aiResponseLimitExceeded,
@JsonValue('AIImageResponseLimitExceeded')
aiImageResponseLimitExceeded,
@JsonValue('Other')
other,
}
@@ -1,11 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:http/http.dart' as http;
import 'ai_client.dart';
import 'error.dart';
import 'text_completion.dart';
@@ -160,6 +160,7 @@ class HttpOpenAIRepository implements AIRepository {
@override
Future<void> streamCompletion({
String? objectId,
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
@@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'text_completion.freezed.dart';
part 'text_completion.g.dart';
@@ -1,18 +1,16 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.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_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// An animated generating indicator for an AI response
class ChatAILoading extends StatelessWidget {
const ChatAILoading({
class AILoadingIndicator extends StatelessWidget {
const AILoadingIndicator({
super.key,
this.text = "",
this.duration = const Duration(seconds: 1),
});
final String text;
final Duration duration;
@override
@@ -27,14 +25,9 @@ class ChatAILoading extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 4.0),
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
builder: (context, state) {
return FlowyText(
state.progress?.step ??
LocaleKeys.chat_generatingResponse.tr(),
color: Theme.of(context).hintColor,
);
},
child: FlowyText(
text,
color: Theme.of(context).hintColor,
),
),
buildDot(const Color(0xFF9327FF))
@@ -6,9 +6,7 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import '../layout_define.dart';
enum SendButtonState { enabled, streaming, disabled }
import 'layout_define.dart';
class PromptInputAttachmentButton extends StatelessWidget {
const PromptInputAttachmentButton({required this.onTap, super.key});
@@ -67,57 +65,3 @@ class PromptInputMentionButton extends StatelessWidget {
);
}
}
class PromptInputSendButton extends StatelessWidget {
const PromptInputSendButton({
super.key,
required this.buttonSize,
required this.iconSize,
required this.state,
required this.onSendPressed,
required this.onStopStreaming,
});
final double buttonSize;
final double iconSize;
final SendButtonState state;
final VoidCallback onSendPressed;
final VoidCallback onStopStreaming;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
width: buttonSize,
icon: switch (state) {
SendButtonState.enabled => FlowySvg(
FlowySvgs.ai_send_filled_s,
size: Size.square(iconSize),
color: Theme.of(context).colorScheme.primary,
),
SendButtonState.disabled => FlowySvg(
FlowySvgs.ai_send_filled_s,
size: Size.square(iconSize),
color: Theme.of(context).disabledColor,
),
SendButtonState.streaming => FlowySvg(
FlowySvgs.ai_stop_filled_s,
size: Size.square(iconSize),
color: Theme.of(context).colorScheme.primary,
),
},
onPressed: () {
switch (state) {
case SendButtonState.enabled:
onSendPressed();
break;
case SendButtonState.streaming:
onStopStreaming();
break;
case SendButtonState.disabled:
break;
}
},
hoverColor: Colors.transparent,
);
}
}
@@ -0,0 +1,53 @@
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:flutter/material.dart';
import 'mentioned_page_text_span.dart';
class PromptInputTextField extends StatelessWidget {
const PromptInputTextField({
super.key,
required this.cubit,
required this.textController,
required this.textFieldFocusNode,
required this.contentPadding,
this.hintText = "",
});
final ChatInputControlCubit cubit;
final TextEditingController textController;
final FocusNode textFieldFocusNode;
final EdgeInsetsGeometry contentPadding;
final String hintText;
@override
Widget build(BuildContext context) {
return ExtendedTextField(
controller: textController,
focusNode: textFieldFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: contentPadding,
hintText: hintText,
hintStyle: AIChatUILayout.inputHintTextStyle(context),
isCollapsed: true,
isDense: true,
),
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
minLines: 1,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
specialTextSpanBuilder: PromptInputTextSpanBuilder(
inputControlCubit: cubit,
specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
);
}
}
@@ -8,10 +8,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import '../layout_define.dart';
import 'layout_define.dart';
class ChatInputFile extends StatelessWidget {
const ChatInputFile({
class PromptInputFile extends StatelessWidget {
const PromptInputFile({
super.key,
required this.chatId,
required this.onDeleted,
@@ -23,7 +23,7 @@ class ChatInputFile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocSelector<AIPromptInputBloc, AIPromptInputState, List<ChatFile>>(
selector: (state) => state.uploadFiles,
selector: (state) => state.attachedFiles,
builder: (context, files) {
if (files.isEmpty) {
return const SizedBox.shrink();
@@ -0,0 +1,42 @@
import 'package:flutter/widgets.dart';
class DesktopAIPromptSizes {
const DesktopAIPromptSizes._();
static const attachedFilesBarPadding =
EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0);
static const attachedFilesPreviewHeight = 48.0;
static const attachedFilesPreviewSpacing = 12.0;
static const predefinedFormatButtonHeight = 28.0;
static const predefinedFormatIconHeight = 16.0;
static const textFieldMinHeight = 36.0;
static const textFieldContentPadding =
EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0);
static const actionBarButtonSize = 28.0;
static const actionBarIconSize = 16.0;
static const actionBarSendButtonSize = 32.0;
static const actionBarSendButtonIconSize = 24.0;
}
class MobileAIPromptSizes {
const MobileAIPromptSizes._();
static const attachedFilesBarHeight = 68.0;
static const attachedFilesBarPadding =
EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0, bottom: 4.0);
static const attachedFilesPreviewHeight = 56.0;
static const attachedFilesPreviewSpacing = 8.0;
static const predefinedFormatButtonHeight = 32.0;
static const predefinedFormatIconHeight = 20.0;
static const textFieldMinHeight = 32.0;
static const textFieldContentPadding =
EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0);
static const mentionIconSize = 20.0;
static const sendButtonSize = 32.0;
}
@@ -0,0 +1,204 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/workspace/application/view/view_service.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:flutter/material.dart';
import 'mention_page_menu.dart';
Future<ViewPB?> showPageSelectorSheet(
BuildContext context, {
required bool Function(ViewPB view) filter,
}) async {
return showMobileBottomSheet<ViewPB>(
context,
backgroundColor: Theme.of(context).colorScheme.surface,
maxChildSize: 0.98,
enableDraggableScrollable: true,
scrollableWidgetBuilder: (context, scrollController) {
return Expanded(
child: _MobilePageSelectorBody(
filter: filter,
scrollController: scrollController,
),
);
},
builder: (context) => const SizedBox.shrink(),
);
}
class _MobilePageSelectorBody extends StatefulWidget {
const _MobilePageSelectorBody({
this.filter,
this.scrollController,
});
final bool Function(ViewPB view)? filter;
final ScrollController? scrollController;
@override
State<_MobilePageSelectorBody> createState() =>
_MobilePageSelectorBodyState();
}
class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> {
final textController = TextEditingController();
late final Future<List<ViewPB>> _viewsFuture = _fetchViews();
@override
void dispose() {
textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: widget.scrollController,
shrinkWrap: true,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _Header(
child: ColoredBox(
color: Theme.of(context).cardColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DragHandle(),
SizedBox(
height: 44.0,
child: Center(
child: FlowyText.medium(
LocaleKeys.document_mobilePageSelector_title.tr(),
fontSize: 16.0,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: SizedBox(
height: 44.0,
child: FlowySearchTextField(
controller: textController,
onChanged: (_) => setState(() {}),
),
),
),
const Divider(height: 0.5, thickness: 0.5),
],
),
),
),
),
FutureBuilder(
future: _viewsFuture,
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SliverToBoxAdapter(
child: CircularProgressIndicator.adaptive(),
);
}
if (snapshot.hasError || snapshot.data == null) {
return SliverToBoxAdapter(
child: FlowyText(
LocaleKeys.document_mobilePageSelector_failedToLoad.tr(),
),
);
}
final views = snapshot.data!
.where((v) => widget.filter?.call(v) ?? true)
.toList();
final filtered = views.where(
(v) =>
textController.text.isEmpty ||
v.name
.toLowerCase()
.contains(textController.text.toLowerCase()),
);
if (filtered.isEmpty) {
return SliverToBoxAdapter(
child: FlowyText(
LocaleKeys.document_mobilePageSelector_noPagesFound.tr(),
),
);
}
return SliverPadding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final view = filtered.elementAt(index);
return InkWell(
onTap: () => Navigator.of(context).pop(view),
borderRadius: BorderRadius.circular(12),
splashColor: Colors.transparent,
child: Container(
height: 44,
padding: const EdgeInsets.all(4.0),
child: Row(
children: [
MentionViewIcon(view: view),
const HSpace(8),
Expanded(
child: MentionViewTitleAndAncestors(view: view),
),
],
),
),
);
},
childCount: filtered.length,
),
),
);
},
),
],
);
}
Future<List<ViewPB>> _fetchViews() async =>
(await ViewBackendService.getAllViews()).toNullable()?.items ?? [];
}
class _Header extends SliverPersistentHeaderDelegate {
const _Header({
required this.child,
});
final Widget child;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return child;
}
@override
double get maxExtent => 120.5;
@override
double get minExtent => 120.5;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
@@ -0,0 +1,433 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.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/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
const double _itemHeight = 44.0;
const double _noPageHeight = 20.0;
const double _fixedWidth = 360.0;
const double _maxHeight = 328.0;
class PromptInputAnchor {
PromptInputAnchor(this.anchorKey, this.layerLink);
final GlobalKey<State<StatefulWidget>> anchorKey;
final LayerLink layerLink;
}
class PromptInputMentionPageMenu extends StatefulWidget {
const PromptInputMentionPageMenu({
super.key,
required this.anchor,
required this.textController,
required this.onPageSelected,
});
final PromptInputAnchor anchor;
final TextEditingController textController;
final void Function(ViewPB view) onPageSelected;
@override
State<PromptInputMentionPageMenu> createState() =>
_PromptInputMentionPageMenuState();
}
class _PromptInputMentionPageMenuState
extends State<PromptInputMentionPageMenu> {
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
context.read<ChatInputControlCubit>().refreshViews();
});
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ChatInputControlCubit, ChatInputControlState>(
builder: (context, state) {
return Stack(
children: [
CompositedTransformFollower(
link: widget.anchor.layerLink,
showWhenUnlinked: false,
offset: Offset(getPopupOffsetX(), 0.0),
followerAnchor: Alignment.bottomLeft,
child: Container(
constraints: const BoxConstraints(
minWidth: _fixedWidth,
maxWidth: _fixedWidth,
maxHeight: _maxHeight,
),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(6.0),
boxShadow: const [
BoxShadow(
color: Color(0x0A1F2329),
blurRadius: 24,
offset: Offset(0, 8),
spreadRadius: 8,
),
BoxShadow(
color: Color(0x0A1F2329),
blurRadius: 12,
offset: Offset(0, 6),
),
BoxShadow(
color: Color(0x0F1F2329),
blurRadius: 8,
offset: Offset(0, 4),
spreadRadius: -8,
),
],
),
child: TextFieldTapRegion(
child: PromptInputMentionPageList(
onPageSelected: widget.onPageSelected,
),
),
),
),
],
);
},
);
}
double getPopupOffsetX() {
if (widget.anchor.anchorKey.currentContext == null) {
return 0.0;
}
final cubit = context.read<ChatInputControlCubit>();
if (cubit.filterStartPosition == -1) {
return 0.0;
}
final textPosition = TextPosition(offset: cubit.filterEndPosition);
final renderBox =
widget.anchor.anchorKey.currentContext?.findRenderObject() as RenderBox;
final textPainter = TextPainter(
text: TextSpan(text: cubit.formatIntputText(widget.textController.text)),
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: renderBox.size.width,
maxWidth: renderBox.size.width,
);
final caretOffset = textPainter.getOffsetForCaret(textPosition, Rect.zero);
final boxes = textPainter.getBoxesForSelection(
TextSelection(
baseOffset: textPosition.offset,
extentOffset: textPosition.offset,
),
);
if (boxes.isNotEmpty) {
return boxes.last.right;
}
return caretOffset.dx;
}
}
class PromptInputMentionPageList extends StatefulWidget {
const PromptInputMentionPageList({
super.key,
required this.onPageSelected,
});
final void Function(ViewPB view) onPageSelected;
@override
State<PromptInputMentionPageList> createState() =>
_PromptInputMentionPageListState();
}
class _PromptInputMentionPageListState
extends State<PromptInputMentionPageList> {
final autoScrollController = SimpleAutoScrollController(
suggestedRowHeight: _itemHeight,
beginGetter: (rect) => rect.top + 8.0,
endGetter: (rect) => rect.bottom - 8.0,
);
@override
void dispose() {
autoScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocConsumer<ChatInputControlCubit, ChatInputControlState>(
listenWhen: (previous, current) {
return previous.maybeWhen(
ready: (_, pFocusedViewIndex) => current.maybeWhen(
ready: (_, cFocusedViewIndex) =>
pFocusedViewIndex != cFocusedViewIndex,
orElse: () => false,
),
orElse: () => false,
);
},
listener: (context, state) {
state.maybeWhen(
ready: (views, focusedViewIndex) {
if (focusedViewIndex == -1 || !autoScrollController.hasClients) {
return;
}
if (autoScrollController.isAutoScrolling) {
autoScrollController.position
.jumpTo(autoScrollController.position.pixels);
}
autoScrollController.scrollToIndex(
focusedViewIndex,
duration: const Duration(milliseconds: 200),
preferPosition: AutoScrollPosition.begin,
);
},
orElse: () {},
);
},
builder: (context, state) {
return state.maybeWhen(
loading: () {
return const Padding(
padding: EdgeInsets.all(8.0),
child: SizedBox(
height: _noPageHeight,
child: Center(
child: CircularProgressIndicator.adaptive(),
),
),
);
},
ready: (views, focusedViewIndex) {
if (views.isEmpty) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: _noPageHeight,
child: Center(
child: FlowyText(
LocaleKeys.chat_inputActionNoPages.tr(),
),
),
),
);
}
return ListView.builder(
shrinkWrap: true,
controller: autoScrollController,
padding: const EdgeInsets.all(8.0),
itemCount: views.length,
itemBuilder: (context, index) {
final view = views[index];
return AutoScrollTag(
key: ValueKey("chat_mention_page_item_${view.id}"),
index: index,
controller: autoScrollController,
child: _ChatMentionPageItem(
view: view,
onTap: () => widget.onPageSelected(view),
isSelected: focusedViewIndex == index,
),
);
},
);
},
orElse: () => const SizedBox.shrink(),
);
},
);
}
}
class _ChatMentionPageItem extends StatelessWidget {
const _ChatMentionPageItem({
required this.view,
required this.isSelected,
required this.onTap,
});
final ViewPB view;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: view.name,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: FlowyHover(
isSelected: () => isSelected,
child: Container(
height: _itemHeight,
padding: const EdgeInsets.all(4.0),
child: Row(
children: [
MentionViewIcon(view: view),
const HSpace(8.0),
Expanded(child: MentionViewTitleAndAncestors(view: view)),
],
),
),
),
),
),
);
}
}
class MentionViewIcon extends StatelessWidget {
const MentionViewIcon({
super.key,
required this.view,
});
final ViewPB view;
@override
Widget build(BuildContext context) {
final spaceIcon = view.buildSpaceIconSvg(context);
if (view.icon.value.isNotEmpty) {
return SizedBox(
width: 16.0,
child: RawEmojiIconWidget(
emoji: view.icon.toEmojiIconData(),
emojiSize: 14,
),
);
}
if (view.isSpace == true && spaceIcon != null) {
return SpaceIcon(
dimension: 16.0,
svgSize: 9.68,
space: view,
cornerRadius: 4,
);
}
return FlowySvg(
view.layout.icon,
size: const Size.square(16),
color: Theme.of(context).hintColor,
);
}
}
class MentionViewTitleAndAncestors extends StatelessWidget {
const MentionViewTitleAndAncestors({
super.key,
required this.view,
});
final ViewPB view;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ViewTitleBarBloc(view: view),
child: BlocBuilder<ViewTitleBarBloc, ViewTitleBarState>(
builder: (context, state) {
final nonEmptyName = view.name.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: view.name;
final ancestorList = _getViewAncestorList(state.ancestors);
if (state.ancestors.isEmpty || ancestorList.trim().isEmpty) {
return FlowyText(
nonEmptyName,
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
nonEmptyName,
fontSize: 14.0,
figmaLineHeight: 20.0,
overflow: TextOverflow.ellipsis,
),
FlowyText(
ancestorList,
fontSize: 12.0,
figmaLineHeight: 16.0,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
],
);
},
),
);
}
/// see workspace/presentation/widgets/view_title_bar.dart, upon which this
/// function was based. This version doesn't include the current view in the
/// result, and returns a string rather than a list of widgets
String _getViewAncestorList(
List<ViewPB> views,
) {
const lowerBound = 2;
final upperBound = views.length - 2;
bool hasAddedEllipsis = false;
String result = "";
if (views.length <= 1) {
return "";
}
// ignore the workspace name, use section name instead in the future
// skip the workspace view
for (var i = 1; i < views.length - 1; i++) {
final view = views[i];
if (i >= lowerBound && i < upperBound) {
if (!hasAddedEllipsis) {
hasAddedEllipsis = true;
result += "… / ";
}
continue;
}
final nonEmptyName = view.name.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: view.name;
result += nonEmptyName;
if (i != views.length - 2) {
// if not the last one, add a divider
result += " / ";
}
}
return result;
}
}
@@ -0,0 +1,78 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:extended_text_library/extended_text_library.dart';
import 'package:flutter/material.dart';
class PromptInputTextSpanBuilder extends SpecialTextSpanBuilder {
PromptInputTextSpanBuilder({
required this.inputControlCubit,
this.specialTextStyle,
});
final ChatInputControlCubit inputControlCubit;
final TextStyle? specialTextStyle;
@override
SpecialText? createSpecialText(
String flag, {
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
int? index,
}) {
if (flag == '') {
return null;
}
if (!isStart(flag, AtText.flag)) {
return null;
}
// index is at the end of the start flag, so the start index should be index - (flag.length - 1)
return AtText(
inputControlCubit,
specialTextStyle ?? textStyle,
onTap,
// scrubbing over text is kinda funky
start: index! - (AtText.flag.length - 1),
);
}
}
class AtText extends SpecialText {
AtText(
this.inputControlCubit,
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap, {
this.start,
}) : super(flag, '', textStyle, onTap: onTap);
static const String flag = '@';
final int? start;
final ChatInputControlCubit inputControlCubit;
@override
bool isEnd(String value) => inputControlCubit.selectedViewIds.contains(value);
@override
InlineSpan finishText() {
final String actualText = toString();
final viewName = inputControlCubit.allViews
.firstWhereOrNull((view) => view.id == actualText.substring(1))
?.name ??
"";
final nonEmptyName = viewName.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: viewName;
return SpecialTextSpan(
text: "@$nonEmptyName",
actualText: actualText,
start: start!,
style: textStyle,
);
}
}
@@ -0,0 +1,209 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
import 'layout_define.dart';
class PromptInputDesktopToggleFormatButton extends StatelessWidget {
const PromptInputDesktopToggleFormatButton({
super.key,
required this.showFormatBar,
required this.predefinedFormat,
required this.onTap,
});
final bool showFormatBar;
final PredefinedFormat predefinedFormat;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
tooltipText: showFormatBar
? LocaleKeys.chat_changeFormat_defaultDescription.tr()
: LocaleKeys.chat_changeFormat_blankDescription.tr(),
width: 28.0,
onPressed: onTap,
icon: showFormatBar
? const FlowySvg(
FlowySvgs.m_aa_text_s,
size: Size.square(16.0),
color: Color(0xFF666D76),
)
: const FlowySvg(
FlowySvgs.ai_text_image_s,
size: Size(21.0, 16.0),
color: Color(0xFF666D76),
),
);
}
}
class ChangeFormatBar extends StatelessWidget {
const ChangeFormatBar({
super.key,
required this.predefinedFormat,
required this.spacing,
required this.onSelectPredefinedFormat,
});
final PredefinedFormat? predefinedFormat;
final double spacing;
final void Function(PredefinedFormat) onSelectPredefinedFormat;
@override
Widget build(BuildContext context) {
return SizedBox(
height: DesktopAIPromptSizes.predefinedFormatButtonHeight,
child: SeparatedRow(
mainAxisSize: MainAxisSize.min,
separatorBuilder: () => HSpace(spacing),
children: [
_buildFormatButton(context, ImageFormat.text),
_buildFormatButton(context, ImageFormat.textAndImage),
_buildFormatButton(context, ImageFormat.image),
if (predefinedFormat?.imageFormat.hasText ?? true) ...[
_buildDivider(),
_buildTextFormatButton(context, TextFormat.auto),
_buildTextFormatButton(context, TextFormat.bulletList),
_buildTextFormatButton(context, TextFormat.numberedList),
_buildTextFormatButton(context, TextFormat.table),
],
],
),
);
}
Widget _buildFormatButton(BuildContext context, ImageFormat format) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (predefinedFormat != null &&
format == predefinedFormat!.imageFormat) {
return;
}
if (format.hasText) {
final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto;
onSelectPredefinedFormat(
PredefinedFormat(imageFormat: format, textFormat: textFormat),
);
} else {
onSelectPredefinedFormat(
PredefinedFormat(imageFormat: format, textFormat: null),
);
}
},
child: FlowyTooltip(
message: format.i18n,
child: SizedBox.square(
dimension: _buttonSize,
child: FlowyHover(
isSelected: () => format == predefinedFormat?.imageFormat,
child: Center(
child: FlowySvg(
format.icon,
size: format == ImageFormat.textAndImage
? Size(21.0 / 16.0 * _iconSize, _iconSize)
: Size.square(_iconSize),
),
),
),
),
),
);
}
Widget _buildDivider() {
return VerticalDivider(
indent: 6.0,
endIndent: 6.0,
width: 1.0 + spacing * 2,
);
}
Widget _buildTextFormatButton(
BuildContext context,
TextFormat format,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (predefinedFormat != null &&
format == predefinedFormat!.textFormat) {
return;
}
onSelectPredefinedFormat(
PredefinedFormat(
imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text,
textFormat: format,
),
);
},
child: FlowyTooltip(
message: format.i18n,
child: SizedBox.square(
dimension: _buttonSize,
child: FlowyHover(
isSelected: () => format == predefinedFormat?.textFormat,
child: Center(
child: FlowySvg(
format.icon,
size: Size.square(_iconSize),
),
),
),
),
),
);
}
double get _buttonSize {
return UniversalPlatform.isMobile
? MobileAIPromptSizes.predefinedFormatButtonHeight
: DesktopAIPromptSizes.predefinedFormatButtonHeight;
}
double get _iconSize {
return UniversalPlatform.isMobile
? MobileAIPromptSizes.predefinedFormatIconHeight
: DesktopAIPromptSizes.predefinedFormatIconHeight;
}
}
class PromptInputMobileToggleFormatButton extends StatelessWidget {
const PromptInputMobileToggleFormatButton({
super.key,
required this.showFormatBar,
required this.onTap,
});
final bool showFormatBar;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 32.0,
child: FlowyButton(
radius: const BorderRadius.all(Radius.circular(8.0)),
margin: EdgeInsets.zero,
expandText: false,
text: showFormatBar
? const FlowySvg(
FlowySvgs.m_aa_text_s,
size: Size.square(20.0),
)
: const FlowySvg(
FlowySvgs.ai_text_image_s,
size: Size(26.25, 20.0),
),
onTap: onTap,
),
);
}
}
@@ -0,0 +1,259 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'select_sources_menu.dart';
class PromptInputMobileSelectSourcesButton extends StatefulWidget {
const PromptInputMobileSelectSourcesButton({
super.key,
required this.onUpdateSelectedSources,
});
final void Function(List<String>) onUpdateSelectedSources;
@override
State<PromptInputMobileSelectSourcesButton> createState() =>
_PromptInputMobileSelectSourcesButtonState();
}
class _PromptInputMobileSelectSourcesButtonState
extends State<PromptInputMobileSelectSourcesButton> {
late final cubit = ChatSettingsCubit();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
cubit.updateSelectedSources(
context.read<ChatBloc>().state.selectedSourceIds,
);
});
}
@override
void dispose() {
cubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
final userProfile = context.read<UserWorkspaceBloc>().userProfile;
final workspaceId = state.currentWorkspace?.workspaceId ?? '';
return MultiBlocProvider(
providers: [
BlocProvider(
key: ValueKey(workspaceId),
create: (context) => SpaceBloc(
userProfile: userProfile,
workspaceId: workspaceId,
)..add(const SpaceEvent.initial(openFirstPage: false)),
),
BlocProvider.value(
value: cubit,
),
],
child: BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
..updateSelectedSources(state.selectedSourceIds)
..updateSelectedStatus();
},
child: FlowyButton(
margin: const EdgeInsetsDirectional.fromSTEB(4, 6, 2, 6),
expandText: false,
text: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowySvg(
FlowySvgs.ai_page_s,
color: Theme.of(context).iconTheme.color,
size: const Size.square(20.0),
),
FlowySvg(
FlowySvgs.ai_source_drop_down_s,
color: Theme.of(context).hintColor,
size: const Size.square(10),
),
],
),
onTap: () async {
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
await showMobileBottomSheet<void>(
context,
backgroundColor: Theme.of(context).colorScheme.surface,
maxChildSize: 0.98,
enableDraggableScrollable: true,
scrollableWidgetBuilder: (_, scrollController) {
return Expanded(
child: BlocProvider.value(
value: cubit,
child: _MobileSelectSourcesSheetBody(
scrollController: scrollController,
),
),
);
},
builder: (context) => const SizedBox.shrink(),
);
if (context.mounted) {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
}
},
),
);
},
),
);
},
);
}
}
class _MobileSelectSourcesSheetBody extends StatefulWidget {
const _MobileSelectSourcesSheetBody({
required this.scrollController,
});
final ScrollController scrollController;
@override
State<_MobileSelectSourcesSheetBody> createState() =>
_MobileSelectSourcesSheetBodyState();
}
class _MobileSelectSourcesSheetBodyState
extends State<_MobileSelectSourcesSheetBody> {
final textController = TextEditingController();
@override
void dispose() {
textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: widget.scrollController,
shrinkWrap: true,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _Header(
child: ColoredBox(
color: Theme.of(context).cardColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DragHandle(),
SizedBox(
height: 44.0,
child: Center(
child: FlowyText.medium(
LocaleKeys.chat_selectSources.tr(),
fontSize: 16.0,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: SizedBox(
height: 44.0,
child: FlowySearchTextField(
controller: textController,
onChanged: (value) => context
.read<ChatSettingsCubit>()
.updateFilter(value),
),
),
),
const Divider(height: 0.5, thickness: 0.5),
],
),
),
),
),
BlocBuilder<ChatSettingsCubit, ChatSettingsState>(
builder: (context, state) {
final sources = state.visibleSources
.where((e) => e.ignoreStatus != IgnoreViewType.hide);
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: sources.length,
(context, index) {
final source = sources.elementAt(index);
return ChatSourceTreeItem(
key: ValueKey(
'visible_select_sources_tree_item_${source.view.id}',
),
chatSource: source,
level: 0,
isDescendentOfSpace: source.view.isSpace,
isSelectedSection: false,
onSelected: (chatSource) {
context
.read<ChatSettingsCubit>()
.toggleSelectedStatus(chatSource);
},
height: 40.0,
);
},
),
);
},
),
],
);
}
}
class _Header extends SliverPersistentHeaderDelegate {
const _Header({
required this.child,
});
final Widget child;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return child;
}
@override
double get maxExtent => 120.5;
@override
double get minExtent => 120.5;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
@@ -0,0 +1,576 @@
import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'layout_define.dart';
import 'mention_page_menu.dart';
class PromptInputDesktopSelectSourcesButton extends StatefulWidget {
const PromptInputDesktopSelectSourcesButton({
super.key,
required this.onUpdateSelectedSources,
});
final void Function(List<String>) onUpdateSelectedSources;
@override
State<PromptInputDesktopSelectSourcesButton> createState() =>
_PromptInputDesktopSelectSourcesButtonState();
}
class _PromptInputDesktopSelectSourcesButtonState
extends State<PromptInputDesktopSelectSourcesButton> {
late final cubit = ChatSettingsCubit();
final popoverController = PopoverController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
cubit.updateSelectedSources(
context.read<ChatBloc>().state.selectedSourceIds,
);
});
}
@override
void dispose() {
cubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
final userProfile = userWorkspaceBloc.userProfile;
final workspaceId =
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceBloc(
userProfile: userProfile,
workspaceId: workspaceId,
)..add(const SpaceEvent.initial(openFirstPage: false)),
),
BlocProvider.value(
value: cubit,
),
],
child: BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
..updateSelectedSources(state.selectedSourceIds)
..updateSelectedStatus();
},
child: AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(320, 380)),
offset: const Offset(0.0, -10.0),
direction: PopoverDirection.topWithCenterAligned,
margin: EdgeInsets.zero,
controller: popoverController,
onOpen: () {
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
},
onClose: () {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
},
popupBuilder: (_) {
return BlocProvider.value(
value: context.read<ChatSettingsCubit>(),
child: const _PopoverContent(),
);
},
child: _IndicatorButton(
onTap: () => popoverController.show(),
),
),
);
},
),
);
}
}
class _IndicatorButton extends StatelessWidget {
const _IndicatorButton({
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
height: DesktopAIPromptSizes.actionBarButtonSize,
child: FlowyHover(
style: const HoverStyle(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowySvg(
FlowySvgs.ai_page_s,
color: Theme.of(context).iconTheme.color,
),
const HSpace(2.0),
BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return FlowyText(
state.selectedSourceIds.length.toString(),
fontSize: 14,
figmaLineHeight: 16,
color: Theme.of(context).hintColor,
);
},
),
const HSpace(2.0),
FlowySvg(
FlowySvgs.ai_source_drop_down_s,
color: Theme.of(context).hintColor,
size: const Size.square(10),
),
],
),
),
),
),
);
}
}
class _PopoverContent extends StatelessWidget {
const _PopoverContent();
@override
Widget build(BuildContext context) {
return BlocBuilder<ChatSettingsCubit, ChatSettingsState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 8),
child: SpaceSearchField(
width: 600,
onSearch: (context, value) =>
context.read<ChatSettingsCubit>().updateFilter(value),
),
),
_buildDivider(),
Flexible(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
children: [
..._buildSelectedSources(context, state),
if (state.selectedSources.isNotEmpty &&
state.visibleSources.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: _buildDivider(),
),
..._buildVisibleSources(context, state),
],
),
),
],
);
},
);
}
Widget _buildDivider() {
return const Divider(
height: 1.0,
thickness: 1.0,
indent: 8.0,
endIndent: 8.0,
);
}
Iterable<Widget> _buildSelectedSources(
BuildContext context,
ChatSettingsState state,
) {
return state.selectedSources
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
.map(
(e) => ChatSourceTreeItem(
key: ValueKey(
'selected_select_sources_tree_item_${e.view.id}',
),
chatSource: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: true,
onSelected: (chatSource) {
context
.read<ChatSettingsCubit>()
.toggleSelectedStatus(chatSource);
},
height: 30.0,
),
);
}
Iterable<Widget> _buildVisibleSources(
BuildContext context,
ChatSettingsState state,
) {
return state.visibleSources
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
.map(
(e) => ChatSourceTreeItem(
key: ValueKey(
'visible_select_sources_tree_item_${e.view.id}',
),
chatSource: e,
level: 0,
isDescendentOfSpace: e.view.isSpace,
isSelectedSection: false,
onSelected: (chatSource) {
context
.read<ChatSettingsCubit>()
.toggleSelectedStatus(chatSource);
},
height: 30.0,
),
);
}
}
class ChatSourceTreeItem extends StatefulWidget {
const ChatSourceTreeItem({
super.key,
required this.chatSource,
required this.level,
required this.isDescendentOfSpace,
required this.isSelectedSection,
required this.onSelected,
this.onAdd,
required this.height,
this.showSaveButton = false,
this.showCheckbox = true,
});
final ChatSource chatSource;
/// nested level of the view item
final int level;
final bool isDescendentOfSpace;
final bool isSelectedSection;
final void Function(ChatSource chatSource) onSelected;
final void Function(ChatSource chatSource)? onAdd;
final bool showSaveButton;
final double height;
final bool showCheckbox;
@override
State<ChatSourceTreeItem> createState() => _ChatSourceTreeItemState();
}
class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
@override
Widget build(BuildContext context) {
final child = SizedBox(
height: widget.height,
child: ChatSourceTreeItemInner(
chatSource: widget.chatSource,
level: widget.level,
isDescendentOfSpace: widget.isDescendentOfSpace,
isSelectedSection: widget.isSelectedSection,
showCheckbox: widget.showCheckbox,
showSaveButton: widget.showSaveButton,
onSelected: widget.onSelected,
onAdd: widget.onAdd,
),
);
final disabledEnabledChild =
widget.chatSource.ignoreStatus == IgnoreViewType.disable
? FlowyTooltip(
message: widget.showCheckbox
? switch (widget.chatSource.view.layout) {
ViewLayoutPB.Document =>
LocaleKeys.chat_sourcesLimitReached.tr(),
_ => LocaleKeys.chat_sourceUnsupported.tr(),
}
: "",
child: Opacity(
opacity: 0.5,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: IgnorePointer(child: child),
),
),
)
: child;
return ValueListenableBuilder(
valueListenable: widget.chatSource.isExpandedNotifier,
builder: (context, isExpanded, child) {
// filter the child views that should be ignored
final childViews = widget.chatSource.children
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
.toList();
if (!isExpanded || childViews.isEmpty) {
return disabledEnabledChild;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
disabledEnabledChild,
...childViews.map(
(childSource) => ChatSourceTreeItem(
key: ValueKey(
'select_sources_tree_item_${childSource.view.id}',
),
chatSource: childSource,
level: widget.level + 1,
isDescendentOfSpace: widget.isDescendentOfSpace,
isSelectedSection: widget.isSelectedSection,
onSelected: widget.onSelected,
height: widget.height,
showCheckbox: widget.showCheckbox,
showSaveButton: widget.showSaveButton,
onAdd: widget.onAdd,
),
),
],
);
},
);
}
}
class ChatSourceTreeItemInner extends StatelessWidget {
const ChatSourceTreeItemInner({
super.key,
required this.chatSource,
required this.level,
required this.isDescendentOfSpace,
required this.isSelectedSection,
required this.showCheckbox,
required this.showSaveButton,
this.onSelected,
this.onAdd,
});
final ChatSource chatSource;
final int level;
final bool isDescendentOfSpace;
final bool isSelectedSection;
final bool showCheckbox;
final bool showSaveButton;
final void Function(ChatSource)? onSelected;
final void Function(ChatSource)? onAdd;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (!isSelectedSection) {
onSelected?.call(chatSource);
}
},
child: FlowyHover(
cursor: isSelectedSection ? SystemMouseCursors.basic : null,
style: HoverStyle(
hoverColor: isSelectedSection
? Colors.transparent
: AFThemeExtension.of(context).lightGreyHover,
),
builder: (context, onHover) {
final isSaveButtonVisible =
showSaveButton && !chatSource.view.isSpace;
final isAddButtonVisible = onAdd != null;
return Row(
children: [
const HSpace(4.0),
HSpace(max(20.0 * level - (isDescendentOfSpace ? 2 : 0), 0)),
// builds the >, ^ or · button
ToggleIsExpandedButton(
chatSource: chatSource,
isSelectedSection: isSelectedSection,
),
const HSpace(2.0),
// checkbox
if (!chatSource.view.isSpace && showCheckbox) ...[
SourceSelectedStatusCheckbox(
chatSource: chatSource,
),
const HSpace(4.0),
],
// icon
MentionViewIcon(
view: chatSource.view,
),
const HSpace(6.0),
// title
Expanded(
child: FlowyText(
chatSource.view.nameOrDefault,
overflow: TextOverflow.ellipsis,
fontSize: 14.0,
figmaLineHeight: 18.0,
),
),
if (onHover && (isSaveButtonVisible || isAddButtonVisible)) ...[
const HSpace(4.0),
if (isSaveButtonVisible)
FlowyIconButton(
tooltipText: LocaleKeys.chat_addToPageButton.tr(),
width: 24,
icon: FlowySvg(
FlowySvgs.ai_add_to_page_s,
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onPressed: () => onSelected?.call(chatSource),
),
if (isSaveButtonVisible && isAddButtonVisible)
const HSpace(4.0),
if (isAddButtonVisible)
FlowyIconButton(
tooltipText: LocaleKeys.chat_addToNewPage.tr(),
width: 24,
icon: FlowySvg(
FlowySvgs.add_less_padding_s,
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onPressed: () => onAdd?.call(chatSource),
),
const HSpace(4.0),
],
],
);
},
),
);
}
}
class ToggleIsExpandedButton extends StatelessWidget {
const ToggleIsExpandedButton({
super.key,
required this.chatSource,
required this.isSelectedSection,
});
final ChatSource chatSource;
final bool isSelectedSection;
@override
Widget build(BuildContext context) {
if (isReferencedDatabaseView(chatSource.view, chatSource.parentView)) {
return const _DotIconWidget();
}
if (chatSource.children.isEmpty) {
return const SizedBox.square(dimension: 16.0);
}
return FlowyHover(
child: GestureDetector(
child: ValueListenableBuilder(
valueListenable: chatSource.isExpandedNotifier,
builder: (context, value, _) => FlowySvg(
value
? FlowySvgs.view_item_expand_s
: FlowySvgs.view_item_unexpand_s,
size: const Size.square(16.0),
),
),
onTap: () => context
.read<ChatSettingsCubit>()
.toggleIsExpanded(chatSource, isSelectedSection),
),
);
}
}
class _DotIconWidget extends StatelessWidget {
const _DotIconWidget();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(6.0),
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).iconTheme.color,
borderRadius: BorderRadius.circular(2),
),
),
);
}
}
class SourceSelectedStatusCheckbox extends StatelessWidget {
const SourceSelectedStatusCheckbox({
super.key,
required this.chatSource,
});
final ChatSource chatSource;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: chatSource.selectedStatusNotifier,
builder: (context, selectedStatus, _) => FlowySvg(
switch (selectedStatus) {
SourceSelectedStatus.unselected => FlowySvgs.uncheck_s,
SourceSelectedStatus.selected => FlowySvgs.check_filled_s,
SourceSelectedStatus.partiallySelected => FlowySvgs.check_partial_s,
},
size: const Size.square(18.0),
blendMode: null,
),
);
}
}
@@ -0,0 +1,70 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
import 'layout_define.dart';
enum SendButtonState { enabled, streaming, disabled }
class PromptInputSendButton extends StatelessWidget {
const PromptInputSendButton({
super.key,
required this.state,
required this.onSendPressed,
required this.onStopStreaming,
});
final SendButtonState state;
final VoidCallback onSendPressed;
final VoidCallback onStopStreaming;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
width: _buttonSize,
icon: switch (state) {
SendButtonState.enabled => FlowySvg(
FlowySvgs.ai_send_filled_s,
size: Size.square(_iconSize),
color: Theme.of(context).colorScheme.primary,
),
SendButtonState.disabled => FlowySvg(
FlowySvgs.ai_send_filled_s,
size: Size.square(_iconSize),
color: Theme.of(context).disabledColor,
),
SendButtonState.streaming => FlowySvg(
FlowySvgs.ai_stop_filled_s,
size: Size.square(_iconSize),
color: Theme.of(context).colorScheme.primary,
),
},
onPressed: () {
switch (state) {
case SendButtonState.enabled:
onSendPressed();
break;
case SendButtonState.streaming:
onStopStreaming();
break;
case SendButtonState.disabled:
break;
}
},
hoverColor: Colors.transparent,
);
}
double get _buttonSize {
return UniversalPlatform.isMobile
? MobileAIPromptSizes.sendButtonSize
: DesktopAIPromptSizes.actionBarSendButtonSize;
}
double get _iconSize {
return UniversalPlatform.isMobile
? MobileAIPromptSizes.sendButtonSize
: DesktopAIPromptSizes.actionBarSendButtonIconSize;
}
}
@@ -58,6 +58,8 @@ class KVKeys {
static const String kCloudType = 'kCloudType';
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
static const String kAppFlowyBaseShareDomain = 'kAppFlowyBaseShareDomain';
static const String kAppFlowyEnableSyncTrace = 'kAppFlowyEnableSyncTrace';
/// The key for saving the text scale factor.
///
@@ -108,4 +110,9 @@ class KVKeys {
///
/// The value is a boolean string
static const String hasUpgradedSpace = 'hasUpgradedSpace060';
/// The key for saving the recent icons
///
/// The value is a json string of [RecentIcons]
static const String recentIcons = 'kRecentIcons';
}
@@ -1,16 +1,24 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:open_filex/open_filex.dart';
import 'package:string_validator/string_validator.dart';
import 'package:url_launcher/url_launcher.dart' as launcher;
typedef OnFailureCallback = void Function(Uri uri);
Future<bool> afLaunchUrl(
/// Launch the uri
///
/// If the uri is a local file path, it will be opened with the OpenFilex.
/// Otherwise, it will be launched with the url_launcher.
Future<bool> afLaunchUri(
Uri uri, {
BuildContext? context,
OnFailureCallback? onFailure,
@@ -18,6 +26,18 @@ Future<bool> afLaunchUrl(
String? webOnlyWindowName,
bool addingHttpSchemeWhenFailed = false,
}) async {
final url = uri.toString();
final decodedUrl = Uri.decodeComponent(url);
// check if the uri is the local file path
if (localPathRegex.hasMatch(decodedUrl)) {
return _afLaunchLocalUri(
uri,
context: context,
onFailure: onFailure,
);
}
// try to launch the uri directly
bool result;
try {
@@ -32,7 +52,7 @@ Future<bool> afLaunchUrl(
}
// if the uri is not a valid url, try to launch it with http scheme
final url = uri.toString();
if (addingHttpSchemeWhenFailed &&
!result &&
!isURL(url, {'require_protocol': true})) {
@@ -54,9 +74,14 @@ Future<bool> afLaunchUrl(
return result;
}
/// Launch the url string
///
/// See [afLaunchUri] for more details.
Future<bool> afLaunchUrlString(
String url, {
bool addingHttpSchemeWhenFailed = false,
BuildContext? context,
OnFailureCallback? onFailure,
}) async {
final Uri uri;
try {
@@ -67,12 +92,56 @@ Future<bool> afLaunchUrlString(
}
// try to launch the uri directly
return afLaunchUrl(
return afLaunchUri(
uri,
addingHttpSchemeWhenFailed: addingHttpSchemeWhenFailed,
context: context,
onFailure: onFailure,
);
}
/// Launch the local uri
///
/// See [afLaunchUri] for more details.
Future<bool> _afLaunchLocalUri(
Uri uri, {
BuildContext? context,
OnFailureCallback? onFailure,
}) async {
final decodedUrl = Uri.decodeComponent(uri.toString());
// open the file with the OpenfileX
var result = await OpenFilex.open(decodedUrl);
if (result.type != ResultType.done) {
// For the file cant be opened, fallback to open the folder
final parentFolder = Directory(decodedUrl).parent.path;
result = await OpenFilex.open(parentFolder);
}
// show the toast if the file is not found
final message = switch (result.type) {
ResultType.done => LocaleKeys.openFileMessage_success.tr(),
ResultType.fileNotFound => LocaleKeys.openFileMessage_fileNotFound.tr(),
ResultType.noAppToOpen => LocaleKeys.openFileMessage_noAppToOpenFile.tr(),
ResultType.permissionDenied =>
LocaleKeys.openFileMessage_permissionDenied.tr(),
ResultType.error => LocaleKeys.failedToOpenUrl.tr(),
};
if (context != null && context.mounted) {
showToastNotification(
context,
message: message,
type: result.type == ResultType.done
? ToastificationType.success
: ToastificationType.error,
);
}
final openFileSuccess = result.type == ResultType.done;
if (!openFileSuccess && onFailure != null) {
onFailure(uri);
Log.error('Failed to open file: $result.message');
}
return openFileSuccess;
}
void _errorHandler(
Uri uri, {
BuildContext? context,
+14
View File
@@ -1,6 +1,8 @@
// ignore_for_file: non_constant_identifier_names
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:json_annotation/json_annotation.dart';
part 'backend_env.g.dart';
@JsonSerializable()
@@ -39,6 +41,8 @@ class AppFlowyCloudConfiguration {
required this.base_url,
required this.ws_base_url,
required this.gotrue_url,
required this.enable_sync_trace,
required this.base_web_domain,
});
factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) =>
@@ -47,6 +51,14 @@ class AppFlowyCloudConfiguration {
final String base_url;
final String ws_base_url;
final String gotrue_url;
final bool enable_sync_trace;
/// The base domain is used in
///
/// - Share URL
/// - Publish URL
/// - Copy Link To Block
final String base_web_domain;
Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);
@@ -55,6 +67,8 @@ class AppFlowyCloudConfiguration {
base_url: '',
ws_base_url: '',
gotrue_url: '',
enable_sync_trace: false,
base_web_domain: ShareConstants.defaultBaseWebDomain,
);
}
+49 -1
View File
@@ -2,6 +2,7 @@ import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/env/backend_env.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
@@ -155,6 +156,13 @@ Future<void> _setAppFlowyCloudUrl(String? url) async {
await getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, url ?? '');
}
Future<void> useBaseWebDomain(String? url) async {
await getIt<KeyValueStorage>().set(
KVKeys.kAppFlowyBaseShareDomain,
url ?? ShareConstants.defaultBaseWebDomain,
);
}
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
await _setAppFlowyCloudUrl(url);
@@ -212,6 +220,8 @@ class AppFlowyCloudSharedEnv {
base_url: Env.afCloudUrl,
ws_base_url: await _getAppFlowyCloudWSUrl(Env.afCloudUrl),
gotrue_url: await _getAppFlowyCloudGotrueUrl(Env.afCloudUrl),
enable_sync_trace: false,
base_web_domain: Env.baseWebDomain,
);
return AppFlowyCloudSharedEnv(
@@ -232,21 +242,29 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
Uri baseUri,
String baseUrl,
AuthenticatorType authenticatorType,
String baseShareDomain,
) async {
// In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports.
// This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server
// in the development environment.
// If you modify following code, please update the corresponding documentation in the appflowy billing.
if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) {
return AppFlowyCloudConfiguration(
base_url: "$baseUrl:8000",
ws_base_url: "ws://${baseUri.host}:8000/ws/v1",
gotrue_url: "$baseUrl:9999",
enable_sync_trace: true,
base_web_domain: ShareConstants.testBaseWebDomain,
);
} else {
return AppFlowyCloudConfiguration(
base_url: baseUrl,
ws_base_url: await _getAppFlowyCloudWSUrl(baseUrl),
gotrue_url: await _getAppFlowyCloudGotrueUrl(baseUrl),
enable_sync_trace: await getSyncLogEnabled(),
base_web_domain: authenticatorType == AuthenticatorType.appflowyCloud
? ShareConstants.defaultBaseWebDomain
: baseShareDomain,
);
}
}
@@ -255,10 +273,16 @@ Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig(
AuthenticatorType authenticatorType,
) async {
final baseURL = await getAppFlowyCloudUrl();
final baseShareDomain = await getAppFlowyShareDomain();
try {
final uri = Uri.parse(baseURL);
return await configurationFromUri(uri, baseURL, authenticatorType);
return await configurationFromUri(
uri,
baseURL,
authenticatorType,
baseShareDomain,
);
} catch (e) {
Log.error("Failed to parse AppFlowy Cloud URL: $e");
return AppFlowyCloudConfiguration.defaultConfig();
@@ -271,6 +295,30 @@ Future<String> getAppFlowyCloudUrl() async {
return result ?? kAppflowyCloudUrl;
}
Future<String> getAppFlowyShareDomain() async {
final result =
await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyBaseShareDomain);
return result ?? ShareConstants.defaultBaseWebDomain;
}
Future<bool> getSyncLogEnabled() async {
final result =
await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyEnableSyncTrace);
if (result == null) {
return false;
}
return result.toLowerCase() == "true";
}
Future<void> setSyncLogEnabled(bool enable) async {
await getIt<KeyValueStorage>().set(
KVKeys.kAppFlowyEnableSyncTrace,
enable.toString().toLowerCase(),
);
}
Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
try {
final uri = Uri.parse(baseURL);
+8
View File
@@ -1,5 +1,6 @@
// lib/env/env.dart
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:envied/envied.dart';
part 'env.g.dart';
@@ -43,4 +44,11 @@ abstract class Env {
defaultValue: '',
)
static const String sentryDsn = _Env.sentryDsn;
@EnviedField(
obfuscate: false,
varName: 'BASE_WEB_DOMAIN',
defaultValue: ShareConstants.defaultBaseWebDomain,
)
static const String baseWebDomain = _Env.baseWebDomain;
}
@@ -21,6 +21,7 @@ extension MobileRouter on BuildContext {
bool showMoreButton = true,
String? fixedTitle,
String? blockId,
List<String>? tabs,
}) async {
// set the current view before pushing the new view
getIt<MenuSharedState>().latestOpenView = view;
@@ -37,6 +38,9 @@ extension MobileRouter on BuildContext {
queryParameters[MobileDocumentScreen.viewBlockId] = blockId;
}
}
if (tabs != null) {
queryParameters[MobileDocumentScreen.viewSelectTabs] = tabs.join('-');
}
final uri = Uri(
path: view.routeName,
@@ -6,7 +6,6 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -26,11 +25,13 @@ class DocumentPageStyleBloc
if (view.id.isEmpty) {
return;
}
final layoutObject =
await ViewBackendService.getView(view.id).fold(
(s) => jsonDecode(s.extra),
(f) => {},
);
Map layoutObject = {};
final data = await ViewBackendService.getView(view.id);
data.onSuccess((s) {
if (s.extra.isNotEmpty) {
layoutObject = jsonDecode(s.extra);
}
});
final fontLayout = _getSelectedFontLayout(layoutObject);
final lineHeightLayout = _getSelectedLineHeightLayout(
layoutObject,
@@ -7,6 +7,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
part 'recent_view_bloc.freezed.dart';
class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
@@ -39,7 +41,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
add(
RecentViewEvent.updateNameOrIcon(
view.name,
view.icon.value,
view.icon.toEmojiIconData(),
),
);
@@ -61,7 +63,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
icon: view.icon.toEmojiIconData(),
),
);
}
@@ -72,7 +74,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
icon: view.icon.toEmojiIconData(),
coverTypeV2: cover.type,
coverValue: cover.value,
),
@@ -82,7 +84,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
icon: view.icon.toEmojiIconData(),
coverTypeV1: coverTypeV1,
coverValue: coverValue,
),
@@ -135,14 +137,17 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
@freezed
class RecentViewEvent with _$RecentViewEvent {
const factory RecentViewEvent.initial() = Initial;
const factory RecentViewEvent.updateCover(
CoverType coverTypeV1, // for the version under 0.5.5, including 0.5.5
CoverType coverTypeV1,
// for the version under 0.5.5, including 0.5.5
PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5
String? coverValue,
) = UpdateCover;
const factory RecentViewEvent.updateNameOrIcon(
String name,
String icon,
EmojiIconData icon,
) = UpdateNameOrIcon;
}
@@ -150,12 +155,12 @@ class RecentViewEvent with _$RecentViewEvent {
class RecentViewState with _$RecentViewState {
const factory RecentViewState({
required String name,
required String icon,
required EmojiIconData icon,
@Default(CoverType.none) CoverType coverTypeV1,
PageStyleCoverImageType? coverTypeV2,
@Default(null) String? coverValue,
}) = _RecentViewState;
factory RecentViewState.initial() =>
const RecentViewState(name: '', icon: '');
RecentViewState(name: '', icon: EmojiIconData.none());
}
@@ -7,11 +7,15 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.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/tab.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -31,6 +35,7 @@ class MobileViewPage extends StatefulWidget {
this.fixedTitle,
this.showMoreButton = true,
this.blockId,
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
});
/// view id
@@ -40,6 +45,7 @@ class MobileViewPage extends StatefulWidget {
final Map<String, dynamic>? arguments;
final bool showMoreButton;
final String? blockId;
final List<PickerTabType> tabs;
// only used in row page
final String? fixedTitle;
@@ -105,6 +111,12 @@ class _MobileViewPageState extends State<MobileViewPage> {
create: (_) =>
ShareBloc(view: view)..add(const ShareEvent.initial()),
),
if (state.userProfilePB != null)
BlocProvider(
create: (_) =>
UserWorkspaceBloc(userProfile: state.userProfilePB!)
..add(const UserWorkspaceEvent.initial()),
),
if (view.layout.isDocumentView)
BlocProvider(
create: (_) => DocumentPageStyleBloc(view: view)
@@ -184,6 +196,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
data: {
MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
MobileDocumentScreen.viewBlockId: widget.blockId,
MobileDocumentScreen.viewSelectTabs: widget.tabs,
},
);
},
@@ -233,6 +246,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
view: view,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
tabs: widget.tabs,
),
]);
}
@@ -255,15 +269,14 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
Widget _buildTitle(BuildContext context, ViewPB? view) {
final icon = view?.icon.value;
final icon = view?.icon;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty) ...[
FlowyText.emoji(
icon,
fontSize: 15.0,
figmaLineHeight: 18.0,
if (icon != null && icon.value.isNotEmpty) ...[
RawEmojiIconWidget(
emoji: icon.toEmojiIconData(),
emojiSize: 15,
),
const HSpace(4),
],
Loaded 100 of 3331 files, more files were not shown because too many files have changed in this diff. Show more