Compare commits

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

This reverts commit 0507c39863.

* chore: bump version 0.8.1

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

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

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

* chore: scroll to end upon sending new message

* chore: bump editor version

* chore: scroll to bottom after adding message

* chore: code reorg

* chore: bump editor version

* chore: bump editor ver

* chore: bump editor

* chore: bump editor

* fix: file block node insertion

* fix: do the same thing on image

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

* fix: missing intrinsic width

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

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

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

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

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

* feat: add hint text next to the web url

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

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

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

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

* chore: use loading builder in network image

* feat: support retry logic in network image

* feat: disable image menu when loading image

* chore: error prompt

---------

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

* feat: support highlighting border when resizing

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

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

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

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

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

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

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

* chore: remove unused code

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

* chore: bump collab version

* chore: bump collab version

* fix: disable deleting mutilple nodes in table

* chore: bump collab version

---------

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

* chore: adjust bottom sheet

* chore: rename and clean up enums

* chore: move all mobile input actions to the bottom

* chore: bump client-api

* chore: connect to API

* chore: apply suggestions from code review

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

* chore: code cleanup

* chore: code cleanup

* chore: update client-api

* chore: expand page

* chore: simplify logic for not displaying related questions

* chore: remove hover effect view icon in select sources

* chore: regenerate with different format

* chore: remove error messages when sending new one

* chore: code style

* chore: bump client api

* fix: image not displaying and hide editing options

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

* chore: fix clippy

* chore: don't add related questions on regenerate

* chore: bump editor

* fix: expand sidebar page

* chore: update client api

---------

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

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

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

* chore: update translation

* feat: support base share domain

* feat: customize web url in login page

* feat: support customizing web url on mobile

* test: change web url test

* fix: cloud integration test

* fix: integration test

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

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

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

* chore: remove unused code

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

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

* chore: add buttons when saving to page

* chore: adjust icon color

* chore: adjust translations

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

* chore: update translations with Fink 🐦

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

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

* fix: error displaying in Favorite/Recent page

* fix: complete the filter logic of icon picker

* fix: the color picker showed when tapping down

* fix: icons are not supported in subpage blocks

* chore: add some tests

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

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

* feat(flutter_desktop): button to expand side peek

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

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

* chore: fix comment

---------

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

* test: fix unit test

* fix: downgrade percent indicator

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

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

* feat: enable auto scroll after inserting column or row

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

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

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

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

* fix: flutter analyze

---------

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

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

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

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

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

* fix: add local image case for versions after 0.5.5

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

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

---------

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

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

* chore: upgrade appflowy_editor

* fix: simple table issues on mobile

* feat: highlight cell after insertion

* test: text color and cell background color test

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

* test: add integration test

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

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

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

* test: add integration tests

* chore(flutter_desktop): make the popoovers as barriers

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

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

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

---------

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

* fix: the icon is not supported in sites tab

* feat: keep the icon panel open after click ramdom

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

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

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

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

* chore: use rust 1.81

* chore: remove build flag

* fix: macos build error

* chore: upgrade super_clipboard

* chore: fix pb lint warning

* chore: fix clippy

---------

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

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

* fix: support select language of code block

* fix: support hover controls of document

* fix: support slash panel of document

* feat: support image block and file block of document

* feat: support outline block of document

* feat: support add document on document

* feat: support create sub-page on document

* feat: support mention panel

* feat: support more actions

* feat: support create space

* fix: add some shortcuts to document

* fix: add some shortcuts to document

* fix: multiple select blocks

* fix: reupdate trash after restore and delete

* fix: lint check

* fix: callout block

* fix: view icon

* fix: colors

* fix: colors

* feat: support add grid/board/calendar

* feat: support database new layout

* fix: slash panel

* fix: bugs

* fix: grid table

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: bugs

* fix: some bugs

* fix: some bugs

* fix: invite members

* fix: styles

* fix: styles

* fix: styles

* fix: inline code

* fix: image block

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: add applink

* fix: add applink

* fix: add applink

* fix: add share link

* fix: inline link

* fix: add log

* fix: simple table bugs

* fix: simple table bugs

* fix: some bugs

* fix: build size

* fix: build size

* fix: remove private from package.json

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: some bugs

* fix: support paste md

* fix: modified toolbar

* fix: modified toolbard actions

* fix: adjust editor toolbar

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

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

* fix: remove cell background color

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

* fix: replace quick action icon color

* fix: add done button in text color menu

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

* fix: table navigation issues and reminder issue

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

* fix: unable to insert page sometimes

* fix: use default name if the title is empty

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

* chore: tooltips for right sidebar actions

* chore: update selection after inserting into existing page

* fix: mobile select sources button background color

* fix: ai response metadata

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

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

* chore: revert podfile changes

* chore: decrease spacing between metadata and hover actions

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

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

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

* refactor: optimize the code

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

---------

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

* chore: apply suggestions from code review

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

* chore: apply suggestions from code review

* chore: reorganize code

* chore: i18n

* chore: enable opening the document in the sidebar

* fix: async await

* chore: rename ai message action bar widget

* feat: make transactions be reflected in the opened document

* chore: don't forget to close the bloc

* fix: isLastLineEmpty

* chore: code cleanup

* fix: sync after EditorState.apply

* chore: decrease visibility of DocumentBlocMap

* chore: add back missing assert

---------

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

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

* test: support plus menu in table cell on mobile

* feat: add lightImpact feedback

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

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

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

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

* refactor: optimize the code

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

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

---------

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

* chore: remove default slash menu items

* feat: support customizing slash commands in table cell

* fix: unable to delete math equation

* fix: underline issue

* feat: support text align in table

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

* fix: reduce the padding in table cell

* fix: drag column / row error in table

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

* chore: update test

* chore: create events

* chore: load csv

* test: add events

* test: clean

* chore: fix test

* chore: fix test

* chore: fix test

* chore: fmt

---------

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

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

* test: update table align

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

* fix: heading padding

* feat: support reordering table

* feat: highlight cell border when dragging

* chore: optimize the highlight border render logic

* fix: table menu error

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

* test: delete column / delete row

* test: delete column / delete row

* test: enable header row / header column

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

* feat: support row/column action sheet

* feat: highlight reorder button

* feat: support table action mobile

* feat: support insert row / insert column

* feat: add more actions in the table bottom sheet

* feat: implement the action logic for table action menu

* feat: support clear content

* feat: enable header column / header row

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

* fix: table cell parse error

* feat: support insert left/insert right

* feat: add delete in table action menu

* feat: support copy a table row/column

* feat: support copy paste in table cell

* feat: support cut in table

* feat: disable cut if no content in clipboard

* chore: refactor table action bottom sheet

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

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

* feat: add align in table bottom sheet

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

* feat: add align action to table action menu

* feat: integrate bold command in table bottom sheet

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

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

* chore: remove unused code

* fix: page style json parse error

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

* feat: support align in table action menu

* feat: add quick actions in table action menu

* chore: update translations

* feat: integrate copy paste in table action menu

* feat: integrate align in table action menu

* fix: flutter ci

* test: add copy/paste/cut test

* fix: align the table with the document title

* fix: flutter tests

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

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

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

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

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-12-20 23:23:59 +08:00
Richard Shiue 7ab68dcc2c chore(flutter_desktop): ai chat ui improvements (#7025)
* chore: improve hover action bar inner radius

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

* chore: restrict number of selected highest-level documents

* chore: ignore chat views

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

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

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

* chore: merge with main

* chore: remove unneeded code

* fix: sqlite migration

* fix: cargo fmt

* feat: use new api to get the member role

---------

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

* chore: find question id instead of assuming

* chore: fix clippy

* chore: show local messages if they were there

* chore: remove duplicate code

* chore: fix loading message

* chore: revert unintended translation key removal

* chore: update translation for ai service unavailable

* chore: fix initial chat message load

---------

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

* feat: mobile

* chore: disable mention page

* chore: disable select sources when on local ai

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

* chore: bump client api

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

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

* fix: cannot preview pasted images

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

* feat: improve tab in table cell

* feat: improve shift+tab in table cell

* fix: unable to edit cell after deleting an image

* fix: inline attribute issue

* fix: disable dragging a block into table

* feat: add distribute column evenly in column action menu

* fix: numbered list icon align in table cell

* feat: add setToPageWidth and distributeColumnEvenly in table menu

* feat: support highlight color

* chore: update editor version

* test: add setToPageWidth and distributeColumnEvenly in table menu

* test: inline attribute issues

* test: add distribute column evenly in column action menu

* test: select all in table

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

* test: improve enter shortcut in table cell

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

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

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

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

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

* chore: adding while scrolling through mention page popup

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

* fix: hide orphan views from mention page

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

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

* test: add cloud test

* chore: clean up code

* test: use nameOrDefault

* test: fix failing test

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

* fix: clean up code and minor fixes

* test: add test w/ load more option

* fix: reinstate pageview & clean unused code

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

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

* feat: expand the table based on the widht percentage

* test: set to page width

* feat: distribute columns evenly

* test: distribute columns evenly

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

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

* feat: add document validator

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

* feat: drag to expand the table row

* fix: integration test

* feat: support drag to expand the table

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

* chore: filter sync log

* chore: enable/disable sync log

* chore: enable/disable sync log

* chore: observer document and folder

* fix: integration test

---------

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

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

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

* chore: remove unused logs

* chore: update editor version

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

* test: cancel inline page reference menu by space

* chore: update editor version

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

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

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

* test: add integration tests

* chore: code cleanup

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

* feat(desktop): implement ai chat side panel

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

* chore: clippy

* chore: rename

* chore: set rag_ids when creating a chat

* chore: clippy

* chore: fix test

* chore: fix test

* chore: fix test

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

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

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

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

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

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

* feat: add draggable in table reorder button

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

* feat: separate gestures for popup menu and drag operations

* feat: support feedback mode for table

* feat: build dummy node to render table feedback

* feat: disable column resize handle when dragging column

* feat: higtlight the cell border when dragging

* fix: unable to reorder in row

* fix: do not rebuild the reorder button when reordering

* feat: add reorder logic and tests

* feat: reorder column

* feat: reorder row

* test: reorder row

* fix: table attributes are broken after reordering

* chore: remove unused listerner

* chore: code refactor

* fix: remove unused code

* feat: support rendering table feedback

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

* chore: bump collab

* chore: fix test compile

* chore: fix test

* chore: remove numeric

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

* fix: adjust simple table font size

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

* chore: improve mention page ui

* chore: just use view pb

* chore: remove chat input menu style

* chore: code cleanup

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

* feat: improve appearance of mention page popup

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

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

* fix: integration test and unit test
2024-12-03 17:27:59 +08:00
3261 changed files with 33576 additions and 144561 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
+88 -1
View File
@@ -1,4 +1,91 @@
# Release Notes
## Version 0.8.1 - 14/01/2025
### New Features:
- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only
- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat
- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language
- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more
- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar
- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile
### Bug Fixes
- Resolved an icon rendering issue in callout blocks, tab bars, and search results
- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails
## Version 0.8.0 - 06/01/2025
### Bug Fixes
- Fixed error displaying in the page style menu
- Fixed filter logic in the icon picker
- Fixed error displaying in the Favorite/Recent page
- Fixed the color picker displaying when tapping down
- Fixed icons not being supported in subpage blocks
- Fixed recent icon functionality in the space icon menu
- Fixed "Insert Below" not auto-scrolling the table
- Fixed a to-do item with an emoji automatically creating a soft break
- Fixed header row/column tap areas being too small
- Fixed simple table alignment not working for items that wrap
- Fixed web content reverting after removing the inline code format on desktop
- Fixed inability to make changes to a row or column in the table when opening a new tab
- Fixed changing the language to CKB-KU causing a gray screen on mobile
## Version 0.7.9 - 30/12/2024
### New Features
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
- Create beautiful documents with 22 content types and markdown support
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
- Invite members to your workspace for seamless collaboration
- Create multiple public/private spaces to better organize your content
- Simple Table is now available on Mobile, designed specifically for mobile devices.
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
- Use '/' to insert a content block into a table cell on Desktop
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
- Add messages to an editable document while chatting with AI side by side
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
- Drag a page from the sidebar into a document to easily mention the page without typing its title
- Paste as plain text, a new option in the right-click paste menu
### Bug Fixes
- Fixed misalignment in numbered lists
- Resolved several bugs in the emoji menu
- Fixed a bug with checklist items
## Version 0.7.8 - 18/12/2024
### New Features
<img width="1068" alt="image" src="https://github.com/user-attachments/assets/cf8bd287-f370-4291-8638-76e2bbf4aaac" />
- Meet Simple Table 2.0:
- Insert a list into a table cell
- Insert images, quotes, callouts, and code blocks into a table cell
- Drag to move rows or columns
- Toggle header rows or columns on/off
- Distribute columns evenly
- Adjust to page width
- Enjoy a new UI/UX for a seamless experience
- Revamped mention page interactions in AI Chat
- Improved AppFlowy AI service
### Bug Fixes
- Fixed an error when opening files in the database in local mode
- Fixed arrow up/down navigation not working for selecting a language in Code Block
- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work
## Version 0.7.7 - 09/12/2024
### Bug Fixes
- Fixed sidebar menu resize regression
- Fixed AI chat loading issues
- Fixed inability to open local files in database
- Fixed mentions remaining in notifications after removal from document
- Fixed event card closing when clicking on empty space
- Fixed keyboard shortcut issues
## Version 0.7.6 - 03/12/2024
### New Features
- Revamped the simple table UI
- Added support for capturing images from camera on mobile
### Bug Fixes
- Improved markdown rendering capabilities in AI writer
- Fixed an issue where pressing Enter on a collapsed toggle list would add an unnecessary new line
- Fixed an issue where creating a document from slash menu could insert content at incorrect position
## Version 0.7.5 - 25/11/2024
### Bug Fixes
- Improved chat response parsing
@@ -939,4 +1026,4 @@ Bug fixes and improvements
- Increased height of action
- CPU performance issue
- Fix potential data parser error
- More foundation work for online collaboration
- More foundation work for online collaboration
+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
-15
View File
@@ -121,20 +121,5 @@
// "request": "launch",
// "program": "[YOUR_APPLICATION_PATH]",
},
{
// https://tauri.app/v1/guides/debugging/vs-code
"type": "lldb",
"request": "launch",
"name": "AF-tauri: Debug backend",
"cargo": {
"args": [
"build",
"--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml",
"--no-default-features"
]
},
"preLaunchTask": "AF: Tauri UI Dev",
"cwd": "${workspaceRoot}/appflowy_tauri/"
},
]
}
-45
View File
@@ -245,51 +245,6 @@
"problemMatcher": [],
"detail": "appflowy_flutter"
},
{
"label": "AF: Tauri UI Build",
"type": "shell",
"command": "pnpm run build",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Tauri UI Dev",
"type": "shell",
"isBackground": true,
"command": "pnpm sync:i18n && pnpm run dev",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Tauri Clean",
"type": "shell",
"command": "cargo make tauri_clean",
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "AF: Tauri Clean + Dev",
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Tauri Clean",
"AF: Tauri UI Dev"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "AF: Tauri ESLint",
"type": "shell",
"command": "npx eslint --fix src",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Generate Env File",
"type": "shell",
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.7.6"
APPFLOWY_VERSION = "0.8.1"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -53,7 +53,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
minSdkVersion 29
targetSdkVersion 34
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -36,7 +36,6 @@
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="appflowy-flutter" />
<!-- <data android:host="login-callback" /> -->
</intent-filter>
</activity>
<!--
@@ -50,7 +49,8 @@
<!-- Permission to read files from external storage (outside application container).
As of Android 12 this permission no longer has any effect. Instead use the
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READM_MEDIA_AUDIO permissions. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Permissions to read media files. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<queries>
@@ -1,16 +1,20 @@
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'document/document_test_runner.dart' as document_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'sidebar/sidebar_rename_untitled_test.dart'
as sidebar_rename_untitled_test;
import 'uncategorized/uncategorized_test_runner.dart'
as uncategorized_test_runner;
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
Future<void> main() async {
preset_af_cloud_env_test.main();
data_migration_test_runner.main();
// uncategorized
uncategorized_test_runner.main();
@@ -22,4 +26,6 @@ Future<void> main() async {
// sidebar
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
}
@@ -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);
});
});
}
@@ -249,7 +249,7 @@ More actions for published page:
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
await tester.wait(2000);
// check if the page is published in sites page
final pageItem = find.byWidgetPredicate(
@@ -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();
@@ -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';
@@ -27,10 +26,13 @@ void main() {
await tester.pasteContent(
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
(editorState) {
expect(editorState.document.root.children.length, 3);
expect(editorState.document.root.children.length, 1);
final text =
editorState.document.root.children.first.delta!.toPlainText();
final textLines = text.split('\n');
for (var i = 0; i < lines; i++) {
expect(
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
textLines[i],
'line $i',
);
}
@@ -467,6 +469,44 @@ void main() {
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});
const testMarkdownText = '''
# I'm h1
## I'm h2
### I'm h3
#### I'm h4
##### I'm h5
###### I'm h6''';
testWidgets('paste markdowns', (tester) async {
await tester.pasteContent(
plainText: testMarkdownText,
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (int i = 1; i <= children.length; i++) {
final text = children[i - 1].delta!.toPlainText();
expect(text, 'I\'m h$i');
}
},
);
});
testWidgets('paste markdowns as plain', (tester) async {
await tester.pasteContent(
plainText: testMarkdownText,
pasteAsPlain: true,
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (int i = 1; i <= children.length; i++) {
final text = children[i - 1].delta!.toPlainText();
final expectText = '${'#' * i} I\'m h$i';
expect(text, expectText);
}
},
);
});
}
extension on WidgetTester {
@@ -476,6 +516,7 @@ extension on WidgetTester {
String? plainText,
String? html,
String? inAppJson,
bool pasteAsPlain = false,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
@@ -502,6 +543,7 @@ extension on WidgetTester {
await simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isShiftPressed: pasteAsPlain,
isMetaPressed: Platform.isMacOS,
);
await pumpAndSettle();
@@ -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,212 @@
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/emoji.dart';
import '../../shared/expectation.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final emoji = EmojiIconData.emoji('😁');
const emoji = '😁';
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
group('Icon:', () {
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
tearDownAll(() {
RecentIcons.enable = true;
});
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
testWidgets('Update page emoji in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// update its icon
await tester.updatePageIconInSidebarByName(
name: value.name,
parentName: gettingStarted,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
});
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
testWidgets('Update page icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// update its emoji
await tester.updatePageIconInSidebarByName(
name: value.name,
parentName: gettingStarted,
layout: value,
icon: emoji,
);
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
}
});
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
testWidgets('Update page emoji in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
tester.expectViewTitleHasIcon(
value.name,
value,
emoji,
);
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
});
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its emoji
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
tester.expectViewTitleHasIcon(
value.name,
value,
emoji,
);
}
});
testWidgets('Emoji Search Bar Get Focus', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
await tester.openPage(
value.name,
layout: value,
);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(value.name),
);
await tester.tapButton(title);
await tester.tapButton(find.byType(EmojiPickerButton));
final emojiPicker = find.byType(FlowyEmojiPicker);
expect(emojiPicker, findsOneWidget);
final textField = find.descendant(
of: emojiPicker,
matching: find.byType(FlowyTextField),
);
expect(textField, findsOneWidget);
final textFieldWidget =
textField.evaluate().first.widget as FlowyTextField;
assert(textFieldWidget.focusNode!.hasFocus);
await tester.tapEmoji(emoji.emoji);
await tester.pumpAndSettle();
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
}
});
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInSidebarByName(
name: value.name,
parentName: gettingStarted,
layout: value,
icon: iconData,
);
tester.expectViewHasIcon(
value.name,
value,
iconData,
);
}
});
testWidgets('Update page icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: iconData,
);
tester.expectViewHasIcon(
value.name,
value,
iconData,
);
tester.expectViewTitleHasIcon(
value.name,
value,
iconData,
);
}
});
}
@@ -0,0 +1,56 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/expectation.dart';
void main() {
testWidgets('Skip the empty group name icon in recent icons', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// clear local data
RecentIcons.clear();
await loadIconGroups();
final groups = kIconGroups!;
final List<RecentIcon> localIcons = [];
for (final e in groups) {
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
}
await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, ''));
await tester.openPage(gettingStarted);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(gettingStarted),
);
await tester.tapButton(title);
/// tap emoji picker button
await tester.tapButton(find.byType(EmojiPickerButton));
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
/// tap icon tab
final pickTab = find.byType(PickerTab);
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.icon.tr),
);
await tester.tapButton(iconTab);
expect(find.byType(FlowyIconPicker), findsOneWidget);
/// no recent icons
final recentText = find.descendant(
of: find.byType(FlowyIconPicker),
matching: find.text('Recent'),
);
expect(recentText, findsNothing);
});
}
@@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_recent_icon_test.dart' as sidebar_recent_icon_test;
import 'sidebar_test.dart' as sidebar_test;
import 'sidebar_view_item_test.dart' as sidebar_view_item_test;
@@ -14,4 +15,5 @@ void main() {
sidebar_favorite_test.main();
sidebar_icon_test.main();
sidebar_view_item_test.main();
sidebar_recent_icon_test.main();
}
@@ -1,5 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -11,7 +13,14 @@ import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('Sidebar view item tests', () {
testWidgets('Access view item context menu by right click', (tester) async {
@@ -38,7 +47,11 @@ void main() {
await tester.tapEmoji(emoji);
await tester.pumpAndSettle();
tester.expectViewHasIcon(gettingStarted, ViewLayoutPB.Document, emoji);
tester.expectViewHasIcon(
gettingStarted,
ViewLayoutPB.Document,
EmojiIconData.emoji(emoji),
);
});
});
}
@@ -0,0 +1,91 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/document_test_operations.dart';
import '../document/document_codeblock_paste_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Code Block Language Selector Test', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// create a new document
await tester.createNewPageWithNameUnderParent();
/// tap editor to get focus
await tester.tapButton(find.byType(AppFlowyEditor));
expect(find.byType(CodeBlockLanguageSelector), findsNothing);
await insertCodeBlockInDocument(tester);
///tap button
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
await tester
.tapButtonWithName(LocaleKeys.document_codeBlock_language_auto.tr());
expect(find.byType(CodeBlockLanguageSelector), findsOneWidget);
for (var i = 0; i < 3; ++i) {
await onKey(tester, LogicalKeyboardKey.arrowDown);
}
for (var i = 0; i < 2; ++i) {
await onKey(tester, LogicalKeyboardKey.arrowUp);
}
await onKey(tester, LogicalKeyboardKey.enter);
final editorState = tester.editor.getCurrentEditorState();
String language = editorState
.getNodeAtPath([0])!
.attributes[CodeBlockKeys.language]
.toString();
expect(
language.toLowerCase(),
defaultCodeBlockSupportedLanguages.first.toLowerCase(),
);
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
await tester.tapButtonWithName(language);
await onKey(tester, LogicalKeyboardKey.arrowUp);
await onKey(tester, LogicalKeyboardKey.enter);
language = editorState
.getNodeAtPath([0])!
.attributes[CodeBlockKeys.language]
.toString();
expect(
language.toLowerCase(),
defaultCodeBlockSupportedLanguages.last.toLowerCase(),
);
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
await tester.tapButtonWithName(language);
tester.testTextInput.enterText("rust");
await onKey(tester, LogicalKeyboardKey.delete);
await onKey(tester, LogicalKeyboardKey.delete);
await onKey(tester, LogicalKeyboardKey.arrowDown);
tester.testTextInput.enterText("st");
await onKey(tester, LogicalKeyboardKey.arrowDown);
await onKey(tester, LogicalKeyboardKey.enter);
language = editorState
.getNodeAtPath([0])!
.attributes[CodeBlockKeys.language]
.toString();
expect(language.toLowerCase(), 'rust');
});
}
Future<void> onKey(WidgetTester tester, LogicalKeyboardKey key) async {
await tester.simulateKeyEvent(key);
await tester.pumpAndSettle();
}
@@ -1,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,6 +1,8 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/uncategorized/code_block_language_selector_test.dart'
as code_language_selector;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
@@ -11,6 +13,6 @@ Future<void> runIntegration9OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
tabs_test.main();
code_language_selector.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();
}
@@ -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);
});
}
@@ -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();
}
@@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester {
}
}
Future<void> tapDown(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
PointerDeviceKind kind = PointerDeviceKind.touch,
bool pumpAndSettle = true,
int milliseconds = 500,
}) async {
final location = getCenter(finder);
final TestGesture gesture = await startGesture(
location,
pointer: pointer,
buttons: buttons,
kind: kind,
);
await gesture.cancel();
await gesture.down(location);
await gesture.cancel();
if (pumpAndSettle) {
await this.pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 15),
);
}
}
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,
@@ -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,11 @@ extension CommonOperations on WidgetTester {
);
await tapButton(title);
await tapButton(find.byType(EmojiPickerButton));
await tapEmoji(icon);
if (icon.type == FlowyIconType.emoji) {
await tapEmoji(icon.emoji);
} else if (icon.type == FlowyIconType.icon) {
await tapIcon(icon);
}
await pumpAndSettle();
}
@@ -841,6 +869,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,12 @@
import 'dart:convert';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -11,4 +20,43 @@ extension EmojiTestExtension on WidgetTester {
);
await tapButton(emojiWidget);
}
Future<void> tapIcon(EmojiIconData icon) async {
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
final pickTab = find.byType(PickerTab);
expect(pickTab, findsOneWidget);
await pumpAndSettle();
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.icon.tr),
);
expect(iconTab, findsOneWidget);
await tapButton(iconTab);
final selectedSvg = find.descendant(
of: find.byType(FlowyIconPicker),
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
await tapButton(selectedSvg.first);
final colorPicker = find.byType(IconColorPicker);
expect(colorPicker, findsOneWidget);
final selectedColor = find.descendant(
of: colorPicker,
matching: find.byWidgetPredicate((w) {
if (w is Container) {
final d = w.decoration;
if (d is ShapeDecoration) {
if (d.color ==
Color(int.parse(iconsData.color ?? builtInSpaceColors.first))) {
return true;
}
}
}
return false;
}),
);
await tapButton(selectedColor);
}
}
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
@@ -5,6 +7,8 @@ import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
@@ -14,6 +18,7 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_platform/universal_platform.dart';
@@ -117,7 +122,7 @@ extension Expectation on WidgetTester {
return;
}
final iconWidget = find.byWidgetPredicate(
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
(widget) => widget is EmojiIconWidget && widget.emoji.emoji == emoji,
);
expect(iconWidget, findsOneWidget);
}
@@ -223,24 +228,52 @@ extension Expectation on WidgetTester {
);
}
void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) {
void expectViewHasIcon(String name, ViewLayoutPB layout, EmojiIconData data) {
final pageName = findPageName(
name,
layout: layout,
);
final icon = find.descendant(
of: pageName,
matching: find.text(emoji),
);
expect(icon, findsOneWidget);
final type = data.type;
if (type == FlowyIconType.emoji) {
final icon = find.descendant(
of: pageName,
matching: find.text(data.emoji),
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.icon) {
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
final icon = find.descendant(
of: pageName,
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
expect(icon, findsOneWidget);
}
}
void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) {
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(emoji),
);
expect(icon, findsOneWidget);
void expectViewTitleHasIcon(
String name,
ViewLayoutPB layout,
EmojiIconData data,
) {
final type = data.type;
if (type == FlowyIconType.emoji) {
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(data.emoji),
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.icon) {
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
expect(icon, findsOneWidget);
}
}
void expectSelectedReminder(ReminderOption option) {
@@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy/ai/error.dart';
import 'package:appflowy/ai/openai_client.dart';
import 'package:appflowy/ai/text_completion.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
+9 -3
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,6 +171,8 @@ 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
@@ -185,21 +190,22 @@ SPEC CHECKSUMS:
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
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>
@@ -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,9 +3,6 @@ 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';
@@ -13,6 +10,10 @@ 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';
class AppFlowyAIService implements AIRepository {
@override
Future<FlowyResult<List<String>, AIError>> generateImage({
@@ -39,6 +40,7 @@ class AppFlowyAIService implements AIRepository {
@override
Future<CompletionStream> streamCompletion({
String? objectId,
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
@@ -52,10 +54,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
@@ -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';
@@ -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,
);
}
+48 -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,6 +242,7 @@ 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
@@ -241,12 +252,18 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
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 +272,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 +294,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;
}
@@ -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,14 @@ 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/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';
@@ -105,6 +108,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)
@@ -255,15 +264,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),
],
@@ -189,7 +189,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
final url = context.read<ShareBloc>().state.url;
if (url.isNotEmpty) {
unawaited(
afLaunchUrl(
afLaunchUri(
Uri.parse(url),
mode: LaunchMode.externalApplication,
),
@@ -98,7 +98,7 @@ class BottomSheetBackButton extends StatelessWidget {
width: 18,
height: 18,
child: FlowySvg(
FlowySvgs.m_app_bar_back_s,
FlowySvgs.m_bottom_sheet_back_s,
),
),
),
@@ -138,7 +138,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
_divider(),
..._buildPublishActions(context),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
@@ -205,8 +204,5 @@ class MobileViewBottomSheetBody extends StatelessWidget {
}
}
Widget _divider() => const Divider(
height: 8.5,
thickness: 0.5,
);
Widget _divider() => const MobileQuickActionDivider();
}
@@ -57,6 +57,7 @@ Future<T?> showMobileBottomSheet<T>(
double maxChildSize = 0.8,
double initialChildSize = 0.51,
double bottomSheetPadding = 0,
bool enablePadding = true,
}) async {
assert(
showHeader ||
@@ -172,14 +173,18 @@ Future<T?> showMobileBottomSheet<T>(
}
// ----- content area -----
// add content padding and extra bottom padding
children.add(
Padding(
padding:
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
child: child,
),
);
if (enablePadding) {
// add content padding and extra bottom padding
children.add(
Padding(
padding:
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
child: child,
),
);
} else {
children.add(child);
}
// ----- content area -----
if (children.length == 1) {
@@ -211,14 +216,21 @@ class BottomSheetHeader extends StatelessWidget {
required this.showDoneButton,
this.onRemove,
this.onDone,
this.onBack,
this.onClose,
});
final String title;
final bool showBackButton;
final bool showCloseButton;
final bool showRemoveButton;
final String title;
final bool showDoneButton;
final VoidCallback? onRemove;
final VoidCallback? onBack;
final VoidCallback? onClose;
final void Function(BuildContext context)? onDone;
@override
@@ -230,14 +242,18 @@ class BottomSheetHeader extends StatelessWidget {
child: Stack(
children: [
if (showBackButton)
const Align(
Align(
alignment: Alignment.centerLeft,
child: BottomSheetBackButton(),
child: BottomSheetBackButton(
onTap: onBack,
),
),
if (showCloseButton)
const Align(
Align(
alignment: Alignment.centerLeft,
child: BottomSheetCloseButton(),
child: BottomSheetCloseButton(
onTap: onClose,
),
),
if (showRemoveButton)
Align(
@@ -7,7 +7,6 @@ import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -113,12 +112,8 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
context,
showDragHandle: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) => SeparatedColumn(
builder: (_) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
separatorBuilder: () => const Divider(
height: 8.5,
thickness: 0.5,
),
children: [
MobileQuickActionButton(
text: LocaleKeys.board_column_renameColumn.tr(),
@@ -132,6 +127,7 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
context.pop();
},
),
const MobileQuickActionDivider(),
MobileQuickActionButton(
text: LocaleKeys.board_column_hideColumn.tr(),
icon: FlowySvgs.hide_s,
@@ -149,7 +149,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
icon: FlowySvgs.duplicate_s,
text: LocaleKeys.button_duplicate.tr(),
),
const Divider(height: 8.5, thickness: 0.5),
const MobileQuickActionDivider(),
MobileQuickActionButton(
onTap: () => showMobileBottomSheet(
context,
@@ -201,7 +201,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
icon: FlowySvgs.add_cover_s,
text: 'Add cover',
),
const Divider(height: 8.5, thickness: 0.5),
const MobileQuickActionDivider(),
MobileQuickActionButton(
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
text: LocaleKeys.button_delete.tr(),
@@ -209,7 +209,6 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
icon: FlowySvgs.trash_s,
iconColor: Theme.of(context).colorScheme.error,
),
const Divider(height: 8.5, thickness: 0.5),
],
),
);
@@ -48,7 +48,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
context.pop();
}
}),
_divider(),
const MobileQuickActionDivider(),
_actionButton(
context,
_Action.duplicate,
@@ -58,7 +58,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
},
!isInline,
),
_divider(),
const MobileQuickActionDivider(),
_actionButton(
context,
_Action.delete,
@@ -68,7 +68,6 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
},
!isInline,
),
_divider(),
],
);
}
@@ -88,8 +87,6 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
enable: enable,
);
}
Widget _divider() => const Divider(height: 8.5, thickness: 0.5);
}
enum _Action {
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/shared/loading.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
@@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.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/startup.dart';
import 'package:appflowy/util/built_in_svgs.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
@@ -228,12 +229,13 @@ class _UserIcon extends StatelessWidget {
fontSize: 26,
),
onTap: () async {
final icon = await context.push<EmojiPickerResult>(
final icon = await context.push<EmojiIconData>(
Uri(
path: MobileEmojiPickerScreen.routeName,
queryParameters: {
MobileEmojiPickerScreen.pageTitle:
LocaleKeys.titleBar_userIcon.tr(),
MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name],
},
).toString(),
);
@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
@@ -110,10 +110,7 @@ class MobileRecentView extends StatelessWidget {
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: state.icon.isNotEmpty
? EmojiText(
emoji: state.icon,
fontSize: 30.0,
)
? RawEmojiIconWidget(emoji: state.icon, emojiSize: 30)
: SizedBox.square(
dimension: 32.0,
child: view.defaultIcon(),
@@ -7,6 +7,7 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
@@ -26,7 +27,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:string_validator/string_validator.dart';
import 'package:time/time.dart';
@@ -118,7 +118,7 @@ class MobileViewPage extends StatelessWidget {
}
Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) {
final supportAvatar = isURL(state.icon);
final supportAvatar = isURL(state.icon.emoji);
if (!supportAvatar) {
return _buildLastViewed(context);
}
@@ -173,23 +173,23 @@ class MobileViewPage extends StatelessWidget {
Widget _buildTitle(BuildContext context, RecentViewState state) {
final name = state.name;
final icon = state.icon;
final fontFamily = Platform.isAndroid || Platform.isLinux
? GoogleFonts.notoColorEmoji().fontFamily
: null;
return RichText(
maxLines: 3,
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: icon,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 17.0,
fontWeight: FontWeight.w600,
fontFamily: fontFamily,
if (icon.isNotEmpty) ...[
WidgetSpan(
child: SizedBox(
width: 20,
child: EmojiIconWidget(
emoji: icon,
emojiSize: 18.0,
),
),
if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)),
),
),
const WidgetSpan(child: HSpace(8.0)),
],
TextSpan(
text: name,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
@@ -14,6 +14,7 @@ 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 'package:universal_platform/universal_platform.dart';
const _kNotificationIconHeight = 36.0;
@@ -264,7 +265,11 @@ class NotificationDocumentContent extends StatelessWidget {
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
customHeadingPadding: EdgeInsets.zero,
customHeadingPadding: UniversalPlatform.isDesktop
? EdgeInsets.zero
: EdgeInsets.symmetric(
vertical: EditorStyleCustomizer.nodeHorizontalPadding,
),
);
return IgnorePointer(
@@ -2,6 +2,8 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/mobile_router.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/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -121,6 +123,7 @@ class InnerMobileViewItem extends StatelessWidget {
final bool isDraggable;
final bool isExpanded;
final bool isFirstChild;
// identify if the view item is rendered as feedback widget inside DraggableItem
final bool isFeedback;
@@ -230,6 +233,7 @@ class SingleMobileInnerViewItem extends StatefulWidget {
final ViewPB view;
final ViewPB? parentView;
final bool isExpanded;
// identify if the view item is rendered as feedback widget inside DraggableItem
final bool isFeedback;
@@ -297,11 +301,9 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
Widget _buildViewIcon() {
final icon = widget.view.icon.value.isNotEmpty
? FlowyText.emoji(
widget.view.icon.value,
fontSize: Platform.isAndroid ? 16.0 : 18.0,
figmaLineHeight: 20.0,
optimizeEmojiAlign: true,
? EmojiIconWidget(
emoji: widget.view.icon.toEmojiIconData(),
emojiSize: Platform.isAndroid ? 16.0 : 18.0,
)
: Opacity(
opacity: 0.7,
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -17,15 +18,15 @@ class RTLSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final layoutDirection =
context.watch<AppearanceSettingsCubit>().state.layoutDirection;
final textDirection =
context.watch<AppearanceSettingsCubit>().state.textDirection;
return MobileSettingItem(
name: LocaleKeys.settings_appearance_textDirection_label.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
_textDirectionLabelText(layoutDirection),
_textDirectionLabelText(textDirection),
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
@@ -39,30 +40,33 @@ class RTLSetting extends StatelessWidget {
showDivider: false,
title: LocaleKeys.settings_appearance_textDirection_label.tr(),
builder: (context) {
final layoutDirection =
context.watch<AppearanceSettingsCubit>().state.layoutDirection;
return Column(
children: [
FlowyOptionTile.checkbox(
text: LocaleKeys.settings_appearance_textDirection_ltr.tr(),
isSelected: layoutDirection == LayoutDirection.ltrLayout,
onTap: () {
context
.read<AppearanceSettingsCubit>()
.setLayoutDirection(LayoutDirection.ltrLayout);
Navigator.pop(context);
},
isSelected: textDirection == AppFlowyTextDirection.ltr,
onTap: () => applyTextDirectionAndPop(
context,
AppFlowyTextDirection.ltr,
),
),
FlowyOptionTile.checkbox(
showTopBorder: false,
text: LocaleKeys.settings_appearance_textDirection_rtl.tr(),
isSelected: layoutDirection == LayoutDirection.rtlLayout,
onTap: () {
context
.read<AppearanceSettingsCubit>()
.setLayoutDirection(LayoutDirection.rtlLayout);
Navigator.pop(context);
},
isSelected: textDirection == AppFlowyTextDirection.rtl,
onTap: () => applyTextDirectionAndPop(
context,
AppFlowyTextDirection.rtl,
),
),
FlowyOptionTile.checkbox(
showTopBorder: false,
text: LocaleKeys.settings_appearance_textDirection_auto.tr(),
isSelected: textDirection == AppFlowyTextDirection.auto,
onTap: () => applyTextDirectionAndPop(
context,
AppFlowyTextDirection.auto,
),
),
],
);
@@ -72,13 +76,26 @@ class RTLSetting extends StatelessWidget {
);
}
String _textDirectionLabelText(LayoutDirection? textDirection) {
String _textDirectionLabelText(AppFlowyTextDirection textDirection) {
switch (textDirection) {
case LayoutDirection.rtlLayout:
case AppFlowyTextDirection.auto:
return LocaleKeys.settings_appearance_textDirection_auto.tr();
case AppFlowyTextDirection.rtl:
return LocaleKeys.settings_appearance_textDirection_rtl.tr();
case LayoutDirection.ltrLayout:
case AppFlowyTextDirection.ltr:
default:
return LocaleKeys.settings_appearance_textDirection_ltr.tr();
}
}
void applyTextDirectionAndPop(
BuildContext context,
AppFlowyTextDirection textDirection,
) {
context.read<AppearanceSettingsCubit>().setTextDirection(textDirection);
context
.read<DocumentAppearanceCubit>()
.syncDefaultTextDirection(textDirection.name);
Navigator.pop(context);
}
}
@@ -6,13 +6,20 @@ import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
enum SelfHostUrlBottomSheetType {
shareDomain,
cloudURL,
}
class SelfHostUrlBottomSheet extends StatefulWidget {
const SelfHostUrlBottomSheet({
super.key,
required this.url,
required this.type,
});
final String url;
final SelfHostUrlBottomSheetType type;
@override
State<SelfHostUrlBottomSheet> createState() => _SelfHostUrlBottomSheetState();
@@ -46,8 +53,10 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
controller: _textFieldController,
keyboardType: TextInputType.text,
validator: (value) {
if (value == null || value.isEmpty) {
return LocaleKeys.settings_mobile_usernameEmptyError.tr();
if (value == null ||
value.isEmpty ||
validateUrl(value).isFailure) {
return LocaleKeys.settings_menu_invalidCloudURLScheme.tr();
}
return null;
},
@@ -74,7 +83,12 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
if (value.isNotEmpty) {
validateUrl(value).fold(
(url) async {
await useSelfHostedAppFlowyCloudWithURL(url);
switch (widget.type) {
case SelfHostUrlBottomSheetType.shareDomain:
await useBaseWebDomain(url);
case SelfHostUrlBottomSheetType.cloudURL:
await useSelfHostedAppFlowyCloudWithURL(url);
}
await runAppFlowy();
},
(err) => Log.error(err),
@@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'setting.dart';
@@ -17,44 +18,106 @@ class SelfHostSettingGroup extends StatefulWidget {
}
class _SelfHostSettingGroupState extends State<SelfHostSettingGroup> {
final future = getAppFlowyCloudUrl();
final future = Future.wait([
getAppFlowyCloudUrl(),
getAppFlowyShareDomain(),
]);
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
if (!snapshot.hasData) {
final data = snapshot.data;
if (!snapshot.hasData || data == null || data.length != 2) {
return const SizedBox.shrink();
}
final url = snapshot.data ?? '';
final url = data[0];
final shareDomain = data[1];
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(),
groupTitle: LocaleKeys.settings_menu_cloudAppFlowy.tr(),
settingItemList: [
MobileSettingItem(
name: url,
onTap: () {
showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.editor_urlHint.tr(),
showCloseButton: true,
showDivider: false,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
builder: (_) {
return SelfHostUrlBottomSheet(
url: url,
);
},
);
},
),
_buildSelfHostField(url),
_buildShareDomainField(shareDomain),
],
);
},
);
}
Widget _buildSelfHostField(String url) {
return MobileSettingItem(
title: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: FlowyText(
LocaleKeys.settings_menu_cloudURL.tr(),
fontSize: 12.0,
color: Theme.of(context).hintColor,
),
),
subtitle: FlowyText(
url,
),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () {
showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.editor_urlHint.tr(),
showCloseButton: true,
showDivider: false,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
builder: (_) {
return SelfHostUrlBottomSheet(
url: url,
type: SelfHostUrlBottomSheetType.cloudURL,
);
},
);
},
);
}
Widget _buildShareDomainField(String shareDomain) {
return MobileSettingItem(
title: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: FlowyText(
LocaleKeys.settings_menu_webURL.tr(),
fontSize: 12.0,
color: Theme.of(context).hintColor,
),
),
subtitle: FlowyText(
shareDomain,
),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () {
showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.editor_urlHint.tr(),
showCloseButton: true,
showDivider: false,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
builder: (_) {
return SelfHostUrlBottomSheet(
url: shareDomain,
type: SelfHostUrlBottomSheetType.shareDomain,
);
},
);
},
);
}
}
@@ -4,41 +4,29 @@ import 'package:flutter/material.dart';
class MobileSettingItem extends StatelessWidget {
const MobileSettingItem({
super.key,
required this.name,
this.name,
this.padding = const EdgeInsets.only(bottom: 4),
this.trailing,
this.leadingIcon,
this.title,
this.subtitle,
this.onTap,
});
final String name;
final String? name;
final EdgeInsets padding;
final Widget? trailing;
final Widget? leadingIcon;
final Widget? subtitle;
final VoidCallback? onTap;
final Widget? title;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: ListTile(
title: Row(
children: [
if (leadingIcon != null) ...[
leadingIcon!,
const HSpace(8),
],
Expanded(
child: FlowyText.medium(
name,
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
),
),
],
),
title: title ?? _buildDefaultTitle(name),
subtitle: subtitle,
trailing: trailing,
onTap: onTap,
@@ -47,4 +35,22 @@ class MobileSettingItem extends StatelessWidget {
),
);
}
Widget _buildDefaultTitle(String? name) {
return Row(
children: [
if (leadingIcon != null) ...[
leadingIcon!,
const HSpace(8),
],
Expanded(
child: FlowyText.medium(
name ?? '',
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
}
@@ -25,36 +25,41 @@ class MobileQuickActionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final iconSize = this.iconSize ?? const Size.square(18);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: InkWell(
onTap: enable ? onTap : null,
borderRadius: BorderRadius.circular(12),
overlayColor:
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
FlowySvg(
icon,
size: iconSize,
color: enable ? iconColor : Theme.of(context).disabledColor,
return InkWell(
onTap: enable ? onTap : null,
overlayColor:
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
FlowySvg(
icon,
size: iconSize,
color: enable ? iconColor : Theme.of(context).disabledColor,
),
HSpace(30 - iconSize.width),
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: enable ? textColor : Theme.of(context).disabledColor,
),
HSpace(30 - iconSize.width),
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: enable ? textColor : Theme.of(context).disabledColor,
),
),
],
),
),
],
),
),
);
}
}
class MobileQuickActionDivider extends StatelessWidget {
const MobileQuickActionDivider({super.key});
@override
Widget build(BuildContext context) {
return const Divider(height: 0.5, thickness: 0.5);
}
}
@@ -5,6 +5,7 @@ import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.da
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -20,18 +21,6 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
_init();
}
ChatInputFileMetadata consumeMetadata() {
final metadata = {
for (final file in state.uploadFiles) file.filePath: file,
};
if (metadata.isNotEmpty) {
add(const AIPromptInputEvent.clear());
}
return metadata;
}
final LocalLLMListener _listener;
@override
@@ -44,21 +33,17 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
on<AIPromptInputEvent>(
(event, emit) {
event.when(
newFile: (String filePath, String fileName) {
final files = [...state.uploadFiles];
final newFile = ChatFile.fromFilePath(filePath);
if (newFile != null) {
files.add(newFile);
emit(state.copyWith(uploadFiles: files));
}
},
updateChatState: (LocalAIChatPB chatState) {
// Only user enable chat with file and the plugin is already running
final supportChatWithFile = chatState.fileEnabled &&
chatState.pluginState.state == RunningStatePB.Running;
final aiType = chatState.pluginState.state == RunningStatePB.Running
? AIType.localAI
: AIType.appflowyAI;
emit(
state.copyWith(
aiType: aiType,
supportChatWithFile: supportChatWithFile,
chatState: chatState,
),
@@ -80,19 +65,37 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
),
);
},
deleteFile: (file) {
final files = List<ChatFile>.from(state.uploadFiles);
attachFile: (filePath, fileName) {
final newFile = ChatFile.fromFilePath(filePath);
if (newFile != null) {
emit(
state.copyWith(
attachedFiles: [...state.attachedFiles, newFile],
),
);
}
},
removeFile: (file) {
final files = [...state.attachedFiles];
files.remove(file);
emit(
state.copyWith(
uploadFiles: files,
attachedFiles: files,
),
);
},
clear: () {
updateMentionedViews: (views) {
emit(
state.copyWith(
uploadFiles: [],
mentionedPages: views,
),
);
},
clearMetadata: () {
emit(
state.copyWith(
attachedFiles: [],
mentionedPages: [],
),
);
},
@@ -126,35 +129,55 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
Log.error,
);
}
Map<String, dynamic> consumeMetadata() {
final metadata = {
for (final file in state.attachedFiles) file.filePath: file,
for (final page in state.mentionedPages) page.id: page,
};
if (metadata.isNotEmpty && !isClosed) {
add(const AIPromptInputEvent.clearMetadata());
}
return metadata;
}
}
@freezed
class AIPromptInputEvent with _$AIPromptInputEvent {
const factory AIPromptInputEvent.newFile(String filePath, String fileName) =
_NewFile;
const factory AIPromptInputEvent.deleteFile(ChatFile file) = _DeleteFile;
const factory AIPromptInputEvent.clear() = _ClearFile;
const factory AIPromptInputEvent.updateChatState(
LocalAIChatPB chatState,
) = _UpdateChatState;
const factory AIPromptInputEvent.updatePluginState(
LocalAIPluginStatePB chatState,
) = _UpdatePluginState;
const factory AIPromptInputEvent.attachFile(
String filePath,
String fileName,
) = _AttachFile;
const factory AIPromptInputEvent.removeFile(ChatFile file) = _RemoveFile;
const factory AIPromptInputEvent.updateMentionedViews(List<ViewPB> views) =
_UpdateMentionedViews;
const factory AIPromptInputEvent.clearMetadata() = _ClearMetadata;
}
@freezed
class AIPromptInputState with _$AIPromptInputState {
const factory AIPromptInputState({
required bool supportChatWithFile,
LocalAIChatPB? chatState,
required List<ChatFile> uploadFiles,
required AIType aiType,
required bool supportChatWithFile,
required LocalAIChatPB? chatState,
required List<ChatFile> attachedFiles,
required List<ViewPB> mentionedPages,
}) = _AIPromptInputState;
factory AIPromptInputState.initial() => const AIPromptInputState(
supportChatWithFile: false,
uploadFiles: [],
aiType: AIType.appflowyAI,
supportChatWithFile: false,
chatState: null,
attachedFiles: [],
mentionedPages: [],
);
}
Loaded 100 of 3261 files, more files were not shown because too many files have changed in this diff. Show more