Compare commits

...
82 Commits
Author SHA1 Message Date
Lucas.Xu 2c669f1b85 chore: bump version 0.2.6 (#2963) 2023-07-11 19:49:48 +08:00
Lucas.Xu b9976b072e fix: unable to cancel the inline math equation format (#2974)
* fix: unable to cancel the inline math equation format

* fix: bold font style used in the document is not suitable

* chore: add flutter clean before executing the release task

* fix: integration test
2023-07-11 19:49:29 +08:00
Felix HäberleandNathan.fooo 409e3bf1e3 feat: update translations (#2823)
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2023-07-10 21:37:22 +08:00
Lucas.Xu ff9b3c56c5 feat:IInline math equation (#2949) 2023-07-09 11:03:22 +08:00
Kilu.He 4c17298432 feat: support dark mode (#2945) 2023-07-07 21:58:15 +08:00
Kilu.He 0401fc4b22 fix: tauri check user failed (#2946) 2023-07-06 16:40:20 +08:00
Mathias Mogensen 4805d0c568 Merge pull request #2939 from MayurSMahajan/fr_toggle_theme_shortcut_2304
feat: toggle theme mode shortcut
2023-07-06 09:34:00 +02:00
Nathan.foooandLucas.Xu edc7933c66 feat: support pg storage (#2935)
* refactor: using tokio-postgres

* chore: update

* chore: update env

* chore: update

* chore: upgrade supabase and add logout button

* refactor: update

* chore: update

* refactor: using message queue to handle the pg connection

* refactor: move test

* refactor: update sql

* chore: create pg database when user login

* chore: update scheme

* chore: generic user service

* chore: update

* chore: create statistics

* chore: create snapshot

* chore: add test

* chore: add database cloud service

* chore: add document cloud service

* chore: update interface

* test: add document test

* refactor: document interface

* chore: fix test

* chore: update

* chore: update test

* test: add test

* test: add test

* test: add test

* chore: update collab rev

* fix: flutter analyzer

* chore: update

* chore: update

* chore: update

* fix: tests

* chore: update

* chore: update collab rev

* ci: rust fmt

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-07-05 20:57:09 +08:00
MayurSMahajan 89ff44b035 test: hotkeys integration test 2023-07-05 17:46:10 +05:30
MayurSMahajan 793884427c feat: toggle theme mode shortcut 2023-07-05 10:58:36 +05:30
Richard Shiue e0ad364fa3 test: improve stability of calendar integration test (#2930) 2023-07-05 11:05:05 +08:00
Richard Shiue 5d76a7864d chore: restore default vscode build task (#2931) 2023-07-05 11:04:18 +08:00
Alex WallenandYijing Huang 323cb3b60f [feat] Allow user to select any Google Font (#2895)
* chore: add label for font selection drop down

* chore: add method to set font family

* feat: add drop down to setting appearance view

* feat: add fontFamily to document appearance cubit

* feat: add bloc provider to root for document appearance style

* feat: syncFont family from setting appearance dialog

* feat: plumbing for font style in editor

* fix: add blocprovider before pushing overlay

* chore: add kv_keys

* fix: use fontFamily in document appearance cubit

* fix: remove bloc providers because bloc is supplied in ancestor

* fix: remove unecessary bloc provider

* chore: add constraints to popover

* chore: add translation for search box

* feat: add levenshtein for string sort

* feat: add search bar view

* refactor: levenshtein

* chore: add tests for levenshtein algorithm

* feat: add unit tests for appearance cubit

* fix: analyzer warnings

* feat: sort by ascending if query is empty

* chore: add test for the font family setting widget

* feat: make comparison case insensitive

* feat: lazy load with listview.builder

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: fonts loaded on open application

* fix: checkmark doesn't show

* fix: try catch before getFont

* fix: clear text editing value on close

* fix: remove autofocus for search text field

* chore: add tests

* feat: use sliver protocol

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: avoid using intrinsic height

Co-authored-by: Yijing Huang <hyj891204@gmail.com>

* fix: extra paren caused build failure

* feat: switch order of font family setting

---------

Co-authored-by: Yijing Huang <hyj891204@gmail.com>
2023-07-04 11:30:38 -10:00
Kilu.He 9fb8f221cf chore: add document test (#2932) 2023-07-04 17:17:25 +08:00
8dfbfe3c42 feat: Dynamically Load Themes in AppFlowy (#2670)
* feat: dynamic theme plugin (init)

* feat: provide fallback color if plugin becomes out of date (transparent)

* feat: use applicationDocumentsDirectory to store plugins

* chore: remove json files

* fix: add toJson to resolve analyzer errors

* fix: analyzer (unused imports)

* feat: add code generation scripts for freezed files (call recursively in packages)

* fix: revert changes to dry generation

* feat: call directly into script

* refactor: scripts try to be stateless :)

* fix: path to code generation in toml

* fix: generate script permissions

* fix: path not correct in generate.sh

* feat: modify execution permissions before executing scripts

* chore: switch order of build_runner and easy_localizations

* fix: fs is not valid duckscript cmd

* chore: clean build_runner before executing

* chore: upgrade freezed and build_runner attempt to resolve InvalidType error

* fix: use exec cmd.exe to chmod

* feat: add task to generate all files

* chore: remove redundant task (Code Gen)

* chore: remove json_annoation to dev_dependencies

* fix: dropped & between commands

* chore: rename file and class to FlowyDynamicPlugin

* fix: dependency hell

* fix: json annotation in colorscheme

* fix: analyzer warnings

* fix: duckscript runner for code generator

* fix: try without setting file permissions

* chore: move file picker to infra

* chore: restructure project directory

* feat: add BLoC components for consumers

* chore: update dependencies in pubspec.yaml file

* fix: file picker imports

* feat: add new translations for features

* feat: add new widgets to render upload

* fix: import

* feat: add text overflow

* feat: use animated switcher

* chore: export FileType

* fix: directory was not created, only files were copied

* chore: separate some logic

* feat: add saveFile to FilePickerService

* fix: analyzer error with unused imports

* feat: add translations for uploading

* feat: add builtins property to apptheme

* feat: add theme preview widget

* fix: upload widgets need to fill whole space and account for overflow

* refactor: do not watch file system for changes

* feat: add deletion confirmation dialog

* feat: add form factor resolution for dyanmic layout

* feat: trigger rebuild only when plugins are loaded

* feat: make all methods static

* chore: remove TODO comment, requires further design

* chore: move models to subfolder

* fix: references to plugin service instance

* fix: rebase errors

* fix: more rebasing errors

* feat: remove multiple themes from one plugin

* refactor: use pattern to resolve widget in settings_appearance_view

* refactor: remove commented code

* feat: add translations

* fix: import error

* refactor: separate concerns a bit more

* fix: bug in toJson serialization code

* feat: add package to use represent memory files

* fix: analyzer warnings

* chore: add translation

* chore: remove unused exceptions

* chore: use join

* chore: add documentation

* feat: add tests on theme

* fix: fix scripts for macOS

* feat: use appFlowyDocumentDirectory

* fix: remove unused import

* fix: imports

* feat: allow plugin service to be passed

* fix: theme tests

* feat: separate themes by built-in and plugin

* fix: rebase change name of appFlowyDocumentDirectory

* chore: add test to check that initial state falls back to initial theme

* chore: theme upload preview widget

* chore: rename to brightness setting

* refactor: appearance for settings appearance view

* feat: change show dialog api and use it

* fix: handle plugin compilation exception when incorrect format supplied

* fix: style of theme upload

* fix: always change state so that ui updates

* chore: style of loading widget

* fix: analyzer errors

* feat: add learn more button to documentation

---------

Co-authored-by: Yijing Huang <hyj891204@gmail.com>
Co-authored-by: nathan <nathan@appflowy.io>
2023-07-03 22:07:11 +08:00
Nathan.fooo 453be8352a chore: add ref (#2927) 2023-07-03 11:14:45 +08:00
Kilu.He 452d7eb6d0 feat: support image block (#2912) 2023-07-03 10:04:40 +08:00
f0d5f51703 fix: windows integration test (#2917)
* fix:windows integration test

* fix: load asset

* fix: windows test

* fix: test

* test: refactor the folder test

---------

Co-authored-by: vedon <vedon.fu@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-07-02 23:37:30 +08:00
Lucas.Xu 2c513be305 chore: bump version 0.2.5 (#2924) 2023-07-02 15:15:57 +08:00
Lucas.Xu 11d05b303d feat: support inserting local image (#2913) 2023-07-02 11:46:45 +08:00
Richard Shiue c870dbeac4 test: add more cover integration tests (#2922) 2023-07-01 23:13:09 +08:00
Richard ShiueandLucas.Xu 3acd36e580 test: fix calendar integration test #2919 (#2920)
* test: fix calendar integration test #2919

* chore: update frontend/appflowy_flutter/integration_test/database_calendar_test.dart

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

* test: fix date cell test

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-07-01 20:29:49 +08:00
Aman Negi 95d4fb6865 feat: add outline block (#2750) 2023-06-29 20:28:30 +08:00
Muhammad Rizwan 30b52a29fd feat: support Inline page reference #2196 (#2898) 2023-06-29 10:04:24 +08:00
Richard Shiue a17d6b7eec test: fix integration test (#2906)
* test: fix calendar integration test

* test: add comment for workaround
2023-06-28 22:11:50 +08:00
Kilu.He 18ed553296 feat: support equation block (#2903) 2023-06-28 12:39:04 +08:00
Richard Shiue 7f74fd6149 refactor: cover node widget code (#2899)
* chore: change initial cover type's name to none

* chore: refactor cover node widget

* chore: use a constant instead of magic value

* fix: make the size of icon hover effect smaller

* chore: improve appearance of selected color

* test: add cover integration tests

* fix: inner ring of selected color in dark mode

* refactor: cover node to document header node

* test: simplify tests

* chore: rename files
2023-06-27 15:17:51 +08:00
LainHE a3e09f6021 chore: update language zh-CN (#2900) 2023-06-27 11:11:09 +08:00
Kilu.He 5e0a0f92ff feat: support text equation (#2876) 2023-06-27 10:44:06 +08:00
Alex Wallen 2a2b5fe246 [feat] Add actions to error widget. (#2865)
* chore: rebase error?

* fix: revert changes in Cargo.toml

* feat: add actions row
2023-06-27 09:55:10 +08:00
rmri f914e7aa33 chore: Update hu-HU.json (#2837)
Fixed typos and added missing items in Hungarian translation.
2023-06-27 09:54:46 +08:00
Alex Wallen 386d3f2ba7 feat github redirect (#2867)
* feat: add GitHub to question bubble

* feat: translate to feedback
2023-06-27 09:53:38 +08:00
Nathan.fooo 88faa0f02a feat: using cached view (#2877) 2023-06-23 23:19:34 +08:00
Lucas.Xu f3d6c66f5c chore: bump version 0.2.4 (#2890) 2023-06-23 15:36:44 +08:00
Lucas.Xu d665153a9f fix: 0.2.3 known issues (#2886)
* fix: option menu position

* fix: rename helps highlight the text for the user

* fix: export as markdown file name invalid

* chore: align the emoji in callout

* fix: leave more space in the editor

* fix: 0.2.3 known issues

* chore: add flutter pub get in flutter.toml
2023-06-23 10:54:35 +08:00
Askarbek Zadaulyandqinluhe eee32110f4 feat: nested views
* chore: remove folder code merge page and folder into navitem component

* chore: test fix

* fix: nav item expand fix

* fix: unfold page and active page

* fix: nav item click area fix

* chore: remove old components

* chore: remove old code

* chore: cell controller reorganize

* chore: nav item optimizations

* fix: add async queue to fix data problem

* chore: change semantics of new folder button

* chore: move row methods to database controller

---------

Co-authored-by: qinluhe <qinluhe.twodog@gmail.com>
2023-06-23 10:17:50 +08:00
Richard Shiue 9834eccc7b fix: remove icon button not working in popover (#2885) 2023-06-23 10:01:06 +08:00
Richard Shiue bef7fe87aa test: add more calendar integration tests (#2883)
* test: make sure that the row details page opens

* test: add more calendar tests
2023-06-22 23:40:49 +08:00
Lucas.Xu feceb430cf chore: optimize CI (#2875) 2023-06-22 21:22:46 +08:00
Nathan.foooandvedon a29c8ab27a fix: cell focus issue on windows (#2880)
* fix: cell focus issue on windows

* fix: try to fix

* fix: cell focus not working on Windows platform there are multiple text cells

* docs: add documentation

* chore: adjust row detail page ui

* test: add test

* test: fix test

---------

Co-authored-by: vedon <vedon.fu@gmail.com>
2023-06-22 20:16:31 +08:00
Nathan.foooandvedon f1bfcb6066 feat: only allow one windows instance (#2879)
Co-authored-by: vedon <vedon.fu@gmail.com>
2023-06-22 13:25:01 +08:00
Richard Shiue 99cfda0a5c test: more integration tests (#2871)
* test: date cell integration tests

* test: single/multi-select integration tests
2023-06-21 22:53:49 +08:00
Lucas.Xu 9bd629aaef fix: undo redo for the transforming block will raise an error (#2869)
* fix: undo redo for the transforming block will raise an error

* test: add golden for editing document

* test: add undo redo test
2023-06-21 19:53:29 +08:00
Nathan.fooo d302f6b3fb chore: adjust ui (#2872) 2023-06-21 19:47:30 +08:00
Nathan.foooandLucas.Xu 5595649f53 chore: add database sort integration test (#2866)
* chore: add sort test

* chore: add tests

* chore: update tests

* fix: switch folder test

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-21 19:10:16 +08:00
Nathan.fooo 7de17ab0b8 chore: bump version 0.2.3 (#2864)
* chore: bump version 0.2.3

* chore: update version in pubspec
2023-06-21 15:13:36 +08:00
Nathan.fooo 13ff38756f feat: put all user data into a folder when choosing a custom storage path (#2861) 2023-06-21 12:15:32 +08:00
Lucas.Xu 197e5b3a79 feat: upgrade editor version to latest 0.1.4 2023-06-21 11:36:50 +08:00
Alex Wallen 5cced92646 [feat] make error widget sexy (#2825)
* feat: add translations

* feat: style error page

* chore: update api usage

* feat: add GitHub redirect

* chore: rebase error?

* chore: remove todo comment

* chore: remove impossible code

* fix: update url launcher invocation

* chore: use new api

* fix: analyzer errors

* fix: revert changes in Cargo.toml
2023-06-21 10:03:39 +08:00
Nathan.fooo e50d708c21 feat: create database view on same database (#2829)
* feat: create database view on same database

* feat: switch tag between views

* fix: calendar tool bar

* fix: set layout setting

* chore: update collab rev

* fix: board layout issue

* test: add integration tests

* test: add calendar start from day test
2023-06-20 23:48:34 +08:00
Nathan.fooo 79fc7c4cfe fix: import markdown file (#2841) 2023-06-19 21:20:53 +08:00
Kilu.He 202cd90711 fix: build tauri app failed in macOS (#2828) 2023-06-17 22:02:03 +08:00
Kilu.He 177f7c4fa3 Support document multiple instance and change doc_id from String to &str (#2808)
* fix: support multiple document

* fix: change the doc_id params to ref

* fix: function to converge subscription state

* fix: mousedown behavior update

* fix: turn to textblock when the enter pressed in empty block

* fix: support cut

* fix: emoji caret

* fix: support slash arrow key

* fix: eslint padding-line-between-statements

* fix: add comment

* fix: block side menu bugs

* fix: support key event to select block menu option

* fix: support side menu arrow key

* fix: optimizate selectOptionByUpDown

* fix: format
2023-06-17 14:25:30 +08:00
Lucas.Xu d986e01d3e chore: use retry action v1.0.36 2023-06-16 20:28:31 +08:00
Daniil Golubev 8a8915c662 chore: update russian localization (#2787)
* feat(locale): add missing lines

* feat(locale): update russian translations
2023-06-16 15:33:48 +08:00
efc857d752 chore: some ui improvements (#2791)
* chore: some ui improvements

* fix: integration test

* feat: language selector on welcome page (#2796)

* feat: add language selector on welcome page

* feat: add hover effect and refactor layout

* test: add basic languge selector testing

* chore: increate place holder width

* fix: add catch error for setLocale and finish the testing

* chore: update comment

* feat: refactor the skip login in page and add tests

---------

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

* feat: row document (#2792)

* chore: create orphan view handler

* feat: save icon url and cover url in view

* feat: implement emoji picker UI

* chore: config ui

* chore: config ui again

* chore: replace RowPB with RowMetaPB to exposing more row information

* fix: compile error

* feat: show emoji in row

* chore: update

* test: insert emoji test

* test: add update emoji test

* test: add remove emoji test

* test: add create field tests

* test: add create row and delete row integration tests

* test: add create row from row menu

* test: document in row detail page

* test: delete, duplicate row in row detail page

* test: check the row count displayed in grid page

* test: rename existing field in grid page

* test: update field type of exisiting field in grid page

* test: delete field test

* test: add duplicate field test

* test: add hide field test

* test: add edit text cell test

* test: add insert text to text cell test

* test: add edit number cell test

* test: add edit multiple number cells

* test: add edit checkbox cell test

* feat: integrate editor into database row

* test: add edit create time and last edit time cell test

* test: add edit date cell by selecting a date test

* chore: remove unused code

* chore: update checklist bg color

* test: add update database layout test

---------

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

* test: fix test

* test: add create select option test

---------

Co-authored-by: Yijing Huang <hyj891204@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
Co-authored-by: nathan <nathan@appflowy.io>
2023-06-16 15:32:28 +08:00
Nathan.fooo 14dee6b797 test: filter integration (#2821)
* test: add checklist filter test

* fix: widget reference to invalid databaseController

* fix: SelectOptionFilterList doesn't expand to fill the CustomScrollView

* test: add single select and multi-select filter test

* ci: set protoc version
2023-06-16 14:32:32 +08:00
Lucas.Xu 63eee8b415 chore: retry codecov 2023-06-16 13:32:24 +08:00
Nathan.fooo d96a1d8bd4 test: import database integration (#2803)
* feat: support importing database raw data

* feat: verify import database test

* test: fix test

* ci: update integration test ci config

* ci: codecov with os flag

* ci: update docker command

* ci: update docker command

* ci: update docker command

* ci: update docker command

* test: add filter test
2023-06-15 22:43:07 +08:00
Lucas.Xu 430325c731 chore: bump version 0.2.2 (#2804) 2023-06-15 18:55:21 +08:00
Lucas.Xu d5884ad2b5 fix: unable insert a reference database (#2798)
* fix: unable insert a reference database

* test: add reference database tests

* feat: set min height for document inside database
2023-06-15 16:33:44 +08:00
Kilu.He 95f8b2e9a4 chore: create redo/undo bridge (#2760)
* chore: create redo/undo bridge

* chore: update test

* chore: review update

* chore: review update

* chore: react redo/undo

* chore: review update

* chore: add test

* chore: review update

* chore: generate document id

* chore: update undo/redo

* chore: update cargo lock
2023-06-15 10:37:51 +08:00
Nathan.foooandLucas.Xu 27dd719aa8 feat: row document (#2792)
* chore: create orphan view handler

* feat: save icon url and cover url in view

* feat: implement emoji picker UI

* chore: config ui

* chore: config ui again

* chore: replace RowPB with RowMetaPB to exposing more row information

* fix: compile error

* feat: show emoji in row

* chore: update

* test: insert emoji test

* test: add update emoji test

* test: add remove emoji test

* test: add create field tests

* test: add create row and delete row integration tests

* test: add create row from row menu

* test: document in row detail page

* test: delete, duplicate row in row detail page

* test: check the row count displayed in grid page

* test: rename existing field in grid page

* test: update field type of exisiting field in grid page

* test: delete field test

* test: add duplicate field test

* test: add hide field test

* test: add edit text cell test

* test: add insert text to text cell test

* test: add edit number cell test

* test: add edit multiple number cells

* test: add edit checkbox cell test

* feat: integrate editor into database row

* test: add edit create time and last edit time cell test

* test: add edit date cell by selecting a date test

* chore: remove unused code

* chore: update checklist bg color

* test: add update database layout test

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-14 22:16:33 +08:00
Yijing HuangandLucas.Xu b8983e4466 feat: language selector on welcome page (#2796)
* feat: add language selector on welcome page

* feat: add hover effect and refactor layout

* test: add basic languge selector testing

* chore: increate place holder width

* fix: add catch error for setLocale and finish the testing

* chore: update comment

* feat: refactor the skip login in page and add tests

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-14 19:14:41 +08:00
Kilu.HeandLucas.Xu 00c0934df6 Support text block add link (#2730)
* feat: support text block href attribute

* fix: double click didn't select range

* fix: link update

* chore: ts lint

* chore: add new line

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

* chore: update get word indices function

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-13 15:59:18 +08:00
Lucas.Xu aca3c90eb3 chore: optimize the integration test UI 2023-06-12 21:53:39 +08:00
Lucas.Xu 8b4d1868e5 test: add integration test 2023-06-12 20:13:25 +08:00
Lucas.Xu 5bcf48a4f9 fix: the markdown file name doesn't update after renaming the document (#2777)
* fix: the markdown file name doesn't update after renaming the document

* test: add integration test
2023-06-12 14:29:09 +08:00
Lucas.Xu 335861706e feat: support importing multiple md files all at once (#2778) 2023-06-12 14:27:38 +08:00
Nathan.foooandLucas.Xu 4f5672c2af refactor: create view interface (#2772)
* refactor: create view interface

* chore: update doc

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

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-12 12:57:01 +08:00
Lucas.Xu b5d9a73194 chore: bump version 0.2.1 (#2762) 2023-06-11 17:02:09 +08:00
Nathan.fooo 619d82f115 chore: bump up collab ref (#2764) 2023-06-11 16:24:41 +08:00
Lucas.Xu d5887b0a12 chore: enable codecov action (#2741)
* chore: enable codecov action

* test: fix integration test

* test: fix switch_folder_test
2023-06-11 14:19:44 +08:00
c7f73551c7 fix: update database layout in document (#2757)
* fix: update database layout in document

* feat: create or ref calendar

* Update frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/calendar/calendar_menu_item.dart

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

* fix: stop listen

* feat: remove the redundant database view files

* chore: rename the grid state

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-11 08:49:10 +08:00
Lucas.Xu ed04e247ba fix: the share markdown feature doesn't work in 0.2.0 (#2756)
* chore: bump version 0.2.0

* fix: the share markdown feature doesn't work in 0.2.0

* test: add integration test for share button
2023-06-10 22:38:25 +08:00
Nathan.fooo 2ecd0a67ad feat: hide url cell accessory when the content is empty (#2754) 2023-06-10 15:48:56 +08:00
Nathan.fooo 4ce94a9ceb fix: adding vertical padding for progress bar (#2753) 2023-06-10 14:57:52 +08:00
Nathan.fooo d4e39389d2 fix: show calendar layout (#2752) 2023-06-10 13:23:29 +08:00
Nathan.fooo ab5a3dae3c chore: upgrade rust version to 1.70 (#2749)
* chore: upgrade rust version to 1.70

* ci: fix wanrings

* ci: fix clippy warings
2023-06-09 22:23:07 +08:00
Nathan.fooo 133ac72317 ci: fix docker ci (#2745)
* ci: fix permission
2023-06-09 19:48:16 +08:00
Nathan.fooo e9be37a961 test: add database event tests (#2744)
* chore: add tests

* test: add tests

* test: add tests
2023-06-09 18:57:29 +08:00
chirag paneliyaandLucas.Xu 0f9f5251f2 feat: added enter icon in add field. (#2707)
* [FR] Added Enter Icon In Add Field.

* Update frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart

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

* Update frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart

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

* Update frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart

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

* fix : missing trailing comma and code enhancement.

* chore: format code

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-06-08 21:07:41 +08:00
849 changed files with 32801 additions and 10010 deletions

No files matched your search

+4 -3
View File
@@ -21,12 +21,13 @@ on:
- unlocked
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-app:
if: github.event.pull_request.draft != true
concurrency:
group: docker_ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- name: Checkout source code
+21 -2
View File
@@ -23,10 +23,15 @@ on:
env:
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.65"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
@@ -68,7 +73,7 @@ jobs:
with:
prefix-key: ${{ matrix.os }}
workspaces: |
frontend/rust-lib
frontend/rust-lib
- uses: davidB/rust-cargo-make@v1
with:
@@ -118,3 +123,17 @@ jobs:
working-directory: frontend
run: |
cargo make dart_unit_test
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with:
action: codecov/codecov-action@v3
with: |
name: appflowy
flags: appflowy_flutter_unit_test
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
attempt_limit: 20
attempt_delay: 10000
+68 -53
View File
@@ -6,57 +6,79 @@ on:
- "main"
- "release/*"
paths:
- "frontend/appflowy_flutter/**"
- ".github/workflows/integration_test.yml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
pull_request:
branches:
- "main"
- "release/*"
paths:
- "frontend/appflowy_flutter/**"
- ".github/workflows/integration_test.yml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.70"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tests:
build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
target: x86_64-unknown-linux-gnu
- os: windows-latest
flutter_profile: development-windows-x86
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: "stable-2022-04-07"
- name: Checkout source code
uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
target: ${{ matrix.target }}
override: true
profile: minimal
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.10.1"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Cache Cargo
uses: actions/cache@v2
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
prefix-key: ${{ matrix.os }}
workspaces: |
frontend/rust-lib
cache-all-crates: true
- name: Cache Rust
id: cache-rust-target
uses: actions/cache@v2
- uses: davidB/rust-cargo-make@v1
with:
path: |
frontend/rust-lib/target
shared-lib/target
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
version: '0.36.6'
- name: Setup Environment
- name: Install prerequisites
working-directory: frontend
run: |
cargo install --force cargo-make
cargo install --force duckscript_cli
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
@@ -69,57 +91,50 @@ jobs:
elif [ "$RUNNER_OS" == "macOS" ]; then
echo 'do nothing'
fi
cargo make appflowy-flutter-deps-tools
shell: bash
- if: steps.cache-cargo.outputs.cache-hit != 'true'
name: Rust Deps
working-directory: frontend
run: |
cargo install cargo-make
cargo make appflowy-flutter-deps-tools
- name: Config Flutter
- name: Enable Flutter Desktop
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
flutter config --enable-linux-desktop
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter config --enable-macos-desktop
elif [ "$RUNNER_OS" == "Windows" ]; then
git config --system core.longpaths true
flutter config --enable-windows-desktop
fi
shell: bash
- name: Build Test lib
- name: Build AppFlowy
working-directory: frontend
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
cargo make --profile development-linux-x86_64 appflowy-dev
elif [ "$RUNNER_OS" == "macOS" ]; then
cargo make --profile development-mac-x86_64 appflowy-dev
elif [ "$RUNNER_OS" == "Windows" ]; then
cargo make --profile development-windows-x86 appflowy-dev
fi
shell: bash
cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
- name: Run AppFlowy tests
- name: Run Flutter integration tests
working-directory: frontend/appflowy_flutter
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
export DISPLAY=:99
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test integration_test/runner.dart -d Linux --coverage --verbose
flutter test integration_test/runner.dart -d Linux --coverage
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter test integration_test/runner.dart -d macOS --coverage --verbose
flutter test integration_test/runner.dart -d macOS --coverage
elif [ "$RUNNER_OS" == "Windows" ]; then
flutter test integration_test/runner.dart -d Windows --coverage --verbose
flutter test integration_test/runner.dart -d Windows --coverage
fi
shell: bash
# - uses: codecov/codecov-action@v3
# with:
# name: appflowy
# flags: appflowy
# env_vars: ${{ matrix.os }}
# fail_ci_if_error: true
# verbose: true
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with:
action: codecov/codecov-action@v3
with: |
name: appflowy
flags: appflowy_flutter_integrateion_test
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
attempt_limit: 20
attempt_delay: 10000
+1 -1
View File
@@ -7,7 +7,7 @@ on:
env:
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.65"
RUST_TOOLCHAIN: "1.70"
jobs:
create-release:
+1 -1
View File
@@ -21,7 +21,7 @@ on:
env:
CARGO_TERM_COLOR: always
RUST_TOOLCHAIN: "1.65"
RUST_TOOLCHAIN: "1.70"
FLUTTER_VERSION: "3.10.1"
jobs:
+1 -1
View File
@@ -12,7 +12,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.10.1"
RUST_TOOLCHAIN: "1.65"
RUST_TOOLCHAIN: "1.70"
jobs:
tests:
+8 -2
View File
@@ -2,16 +2,22 @@ name: Tauri-CI
on:
pull_request:
paths:
- ".github/workflows/tauri_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_tauri/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.65"
RUST_TOOLCHAIN: "1.70"
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:
@@ -38,7 +44,7 @@ jobs:
with:
prefix-key: 'ubuntu-latest-tauri'
workspaces: |
frontend/rust-lib
frontend/rust-lib
frontend/appflowy_tauri/src-tauri
- name: install dependencies (windows only)
+109
View File
@@ -1,24 +1,133 @@
# Release Notes
## Version 0.2.6 - 07/11/2023
### New Features
- Dynamic load themes
- Inline math equation
## Version 0.2.5 - 07/02/2023
### New Features
- Insert local images
- Mention a page
- Outlines (Table of contents)
- Added support for aligning the image by image menu
### Bug fixes
- Some UI issues
## Version 0.2.4 - 06/23/2023
### Bug fixes:
- Unable to copy and paste a word
- Some UI issues
## Version 0.2.3 - 06/21/2023
### New Features
- Added support for creating multiple database views for existing database
## Version 0.2.2 - 06/15/2023
### New Features
- Added support for embedding a document in the database's row detail page
- Added support for inserting an emoji in the database's row detail page
### Other Updates
- Added language selector on the welcome page
- Added support for importing multiple markdown files all at once
## Version 0.2.1 - 06/11/2023
### New Features
- Added support for creating or referencing a calendar in the document
- Added `+` icon in grid's add field
### Other Updates
- Added vertical padding for progress bar
- Hide url cell accessory when the content is empty
### Bug fixes:
- Fixed unable to export markdown
- Fixed adding vertical padding for progress bar
- Fixed database view didn't update after the database layout changed.
## Version 0.2.0 - 06/08/2023
### New Features
- Improved checklists to support each cell having its own list
- Drag and drop calendar events
- Switch layouts (calendar, grid, kanban) of a database
- New database properties: 'Updated At' and 'Created At'
- Enabled hiding properties on the row detail page
- Added support for reordering and saving row order in different database views.
- Enabled each database view to have its own settings, including filter and sort options
- Added support to convert `“` (double quote) into a block quote
- Added support to convert `***` (three stars) into a divider
- Added support for an 'Add' button to insert a paragraph in a document and display the slash menu
- Added support for an 'Option' button to delete, duplicate, and customize block actions
### Other Updates
- Added support for importing v0.1.x documents and databases
- Added support for database import and export to CSV
- Optimized scroll behavior in documents.
- Redesigned the launch page
### Bug fixes
- Fixed bugs related to numbers
- Fixed issues with referenced databases in documents
- Fixed menu overflow issues in documents
### Data migration
The data format of this version is not compatible with previous versions. Therefore, to migrate your data to the new version, you need to use the export and import functions. Please follow the guide to learn how to export and import your data.
#### Export files in v0.1.6
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9
#### Import files in v0.2.0
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2
## Version 0.1.5 - 11/05/2023
### Bug Fixes
- Fix: calendar dates don't match with weekdays.
- Fix: sort numbers in Grid.
## Version 0.1.4 - 04/05/2023
### New features
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
- Writing can be improved with the help of OpenAI.
## Version 0.1.3 - 24/04/2023
### New features
- Launch the official Dark Mode.
- Customize the font color and highlight color by setting a hex color value and an opacity level.
### Bug Fixes
- Fix: the slash menu can be triggered by all other keyboards than English.
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
+31 -10
View File
@@ -118,15 +118,11 @@
{
"label": "AF: Generate Freezed Files",
"type": "shell",
"command": "dart run build_runner build -d",
"command": "sh ./scripts/code_generation/freezed/generate_freezed.sh",
"options": {
"cwd": "${workspaceFolder}/appflowy_flutter"
}
},
{
"label": "AF: Generate Language Files",
"type": "shell",
"command": "sh ./scripts/generate_language_files.sh",
"cwd": "${workspaceFolder}"
},
"group": "build",
"windows": {
"options": {
"shell": {
@@ -134,7 +130,24 @@
"args": [
"/d",
"/c",
".\\scripts\\generate_language_files.cmd"
".\\scripts\\code_generation\\freezed\\generate_freezed.cmd"
]
}
}
},
},
{
"label": "AF: Generate Language Files",
"type": "shell",
"command": "sh ./scripts/code_generation/language_files/generate_language_files.sh",
"windows": {
"options": {
"shell": {
"executable": "cmd.exe",
"args": [
"/d",
"/c",
".\\scripts\\code_generation\\language_files\\generate_language_files.cmd"
]
}
}
@@ -221,5 +234,13 @@
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Generate Env",
"type": "shell",
"command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs ",
"options": {
"cwd": "${workspaceFolder}/appflowy_flutter"
}
},
]
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.2.0"
CURRENT_APP_VERSION = "0.2.6"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
+5
View File
@@ -70,3 +70,8 @@ windows/flutter/dart_ffi/
**/.vscode/
*.env
*.env.*
coverage/
**/failures/*.png
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Calendar / Calendar_Days">
<path id="Vector" d="M8 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V8M8 4H16M8 4V2M16 4H16.8002C17.9203 4 18.4796 4 18.9074 4.21799C19.2837 4.40973 19.5905 4.71547 19.7822 5.0918C20 5.5192 20 6.07899 20 7.19691V8M16 4V2M4 8V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2842 19.7822 18.9079C20 18.4805 20 17.9215 20 16.8036V8M4 8H20M16 16H16.002L16.002 16.002L16 16.002V16ZM12 16H12.002L12.002 16.002L12 16.002V16ZM8 16H8.002L8.00195 16.002L8 16.002V16ZM16.002 12V12.002L16 12.002V12H16.002ZM12 12H12.002L12.002 12.002L12 12.002V12ZM8 12H8.002L8.00195 12.002L8 12.002V12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" style="width: 16px; height: 16px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="30" height="30" ><path d="M6.04883 27.3232C7.24219 27.3232 7.86426 26.4854 8.20703 25.3936L13.9834 6.79492H27.5039C28.2275 6.79492 28.7227 6.35059 28.7227 5.65234C28.7227 4.97949 28.2275 4.53516 27.5039 4.53516H13.958C12.6631 4.53516 12.0791 5.01758 11.7363 6.13477L6.18848 24.3525H5.97266L3.58594 15.9355C3.38281 15.2373 3.00195 14.9072 2.40527 14.9072C1.73242 14.9072 1.2373 15.3896 1.2373 16.0117C1.2373 16.2656 1.30078 16.4941 1.35156 16.6846L4.04297 25.5332C4.36035 26.5615 4.93164 27.3232 6.04883 27.3232ZM16.3955 24.7334C16.8652 24.7334 17.1064 24.5684 17.4619 24.0732L20.4707 19.8203H20.5215L23.5049 24.0732C23.873 24.5684 24.1143 24.7334 24.5713 24.7334C25.2061 24.7334 25.6758 24.3018 25.6758 23.7051C25.6758 23.4258 25.6123 23.1973 25.4219 22.9561L21.9307 18.208L25.4473 13.4346C25.6377 13.168 25.7139 12.9395 25.7139 12.6855C25.7139 12.127 25.2568 11.6953 24.6475 11.6953C24.2031 11.6953 23.9365 11.8477 23.6064 12.3174L20.7246 16.5957H20.6611L17.6904 12.3047C17.373 11.8477 17.0938 11.6953 16.624 11.6953C16.0146 11.6953 15.5068 12.165 15.5068 12.7363C15.5068 13.0537 15.583 13.2568 15.8115 13.5488L19.1504 18.1445L15.6084 23.0322C15.418 23.2861 15.3672 23.4639 15.3672 23.7559C15.3672 24.3018 15.8115 24.7334 16.3955 24.7334Z" fill="#2F3030"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="-0.02 0 122.88 122.88" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="enable-background:new 0 0 122.84 122.88" xml:space="preserve">
<g>
<path d="M81.54,4.71c0-2.62,2.58-4.71,5.77-4.71c3.2,0,5.77,2.13,5.77,4.71V25.4c0,2.62-2.58,4.71-5.77,4.71 c-3.2,0-5.77-2.13-5.77-4.71V4.71L81.54,4.71z M20.4,89.87l7.55-0.47c0.16,1.22,0.5,2.16,1,2.79c0.82,1.04,1.99,1.56,3.51,1.56 c1.13,0,2.01-0.26,2.62-0.8C35.69,92.41,36,91.8,36,91.1c0-0.66-0.29-1.26-0.87-1.79c-0.58-0.53-1.93-1.02-4.06-1.49 c-3.49-0.78-5.96-1.82-7.45-3.12c-1.5-1.29-2.25-2.95-2.25-4.96c0-1.32,0.38-2.56,1.15-3.74c0.77-1.18,1.92-2.1,3.46-2.77 c1.54-0.67,3.65-1.01,6.32-1.01c3.29,0,5.79,0.61,7.52,1.84c1.72,1.22,2.75,3.17,3.08,5.84l-7.47,0.44 c-0.2-1.17-0.62-2.02-1.25-2.55c-0.64-0.53-1.52-0.8-2.64-0.8c-0.92,0-1.62,0.2-2.09,0.59c-0.47,0.39-0.7,0.87-0.7,1.43 c0,0.41,0.19,0.77,0.57,1.1c0.37,0.34,1.25,0.65,2.65,0.95c3.47,0.75,5.96,1.51,7.46,2.28c1.5,0.77,2.6,1.71,3.28,2.85 c0.68,1.13,1.02,2.4,1.02,3.81c0,1.65-0.46,3.17-1.37,4.56c-0.92,1.39-2.19,2.45-3.83,3.17c-1.64,0.72-3.7,1.08-6.19,1.08 c-4.37,0-7.4-0.84-9.09-2.53C21.58,94.6,20.62,92.46,20.4,89.87L20.4,89.87z M64.53,72.65h7.94v15.33c0,1.52-0.24,2.95-0.71,4.3 c-0.47,1.35-1.22,2.53-2.23,3.55c-1.01,1.01-2.07,1.72-3.18,2.13c-1.54,0.57-3.4,0.86-5.56,0.86c-1.25,0-2.62-0.09-4.1-0.26 c-1.48-0.17-2.72-0.52-3.71-1.04c-0.99-0.52-1.9-1.26-2.72-2.22c-0.83-0.96-1.39-1.95-1.69-2.96c-0.49-1.64-0.74-3.08-0.74-4.35 V72.65h7.94v15.69c0,1.4,0.39,2.5,1.16,3.28c0.78,0.79,1.86,1.19,3.23,1.19c1.36,0,2.43-0.39,3.21-1.17 c0.77-0.77,1.16-1.87,1.16-3.3V72.65L64.53,72.65z M77.85,72.65h7.42l9.65,14.21V72.65h7.51v25.73h-7.51l-9.59-14.13v14.13h-7.47 V72.65L77.85,72.65z M29.53,4.71c0-2.62,2.58-4.71,5.77-4.71c3.2,0,5.77,2.13,5.77,4.71V25.4c0,2.62-2.58,4.71-5.77,4.71 c-3.2,0-5.77-2.13-5.77-4.71V4.71L29.53,4.71z M7.56,44.09h107.62V22.66c0-0.8-0.31-1.55-0.84-2.04c-0.53-0.53-1.24-0.84-2.04-0.84 h-9.31c-1.78,0-3.2-2.63-3.2-4.41c0-1.78,1.42-3.2,3.2-3.2h10.53c2.58,0,4.88,1.07,6.57,2.75c1.69,1.69,2.75,4.04,2.75,6.57v92.06 c0,2.58-1.07,4.88-2.75,6.57c-1.69,1.69-4.04,2.75-6.57,2.75H9.33c-2.58,0-4.88-1.07-6.57-2.75C1.07,118.44,0,116.08,0,113.55 V21.49c0-2.58,1.07-4.88,2.75-6.57c1.69-1.69,4.04-2.75,6.57-2.75h11.28c1.78,0,3.2,1.42,3.2,3.2s-1.42,4.41-3.2,4.41H10.54 c-0.8,0-1.55,0.31-2.09,0.84c-0.53,0.53-0.84,1.24-0.84,2.09v21.43L7.56,44.09L7.56,44.09z M115.19,52.9H7.56v59.4 c0,0.8,0.31,1.55,0.84,2.09c0.53,0.53,1.24,0.84,2.09,0.84h101.76c0.8,0,1.55-0.31,2.09-0.84c0.53-0.53,0.84-1.24,0.84-2.09V52.9 L115.19,52.9z M50.36,19.73c-1.78,0-3.2-2.63-3.2-4.41c0-1.78,1.42-3.2,3.2-3.2h21.49c1.78,0,3.2,1.42,3.2,3.2 c0,1.78-1.42,4.41-3.2,4.41H50.36L50.36,19.73z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99999 15.8333C13.2217 15.8333 15.8333 13.2216 15.8333 9.99996C15.8333 6.7783 13.2217 4.16663 9.99999 4.16663C6.77833 4.16663 4.16666 6.7783 4.16666 9.99996C4.16666 13.2216 6.77833 15.8333 9.99999 15.8333Z" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.16666 10H15.8333" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.0002 4.16663C11.4593 5.764 12.2884 7.83698 12.3335 9.99996C12.2884 12.1629 11.4593 14.2359 10.0002 15.8333C8.54109 14.2359 7.7119 12.1629 7.66684 9.99996C7.7119 7.83698 8.54109 5.764 10.0002 4.16663V4.16663Z" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

@@ -0,0 +1,11 @@
"{""id"":""2_OVWb"",""name"":""Name"",""field_type"":0,""visibility"":true,""width"":150,""type_options"":{""0"":{""data"":""""}},""is_primary"":true}","{""id"":""xjmOSi"",""name"":""Type"",""field_type"":3,""visibility"":true,""width"":150,""type_options"":{""3"":{""content"":""{\""options\"":[{\""id\"":\""t1WZ\"",\""name\"":\""s6\"",\""color\"":\""Lime\""},{\""id\"":\""GzNa\"",\""name\"":\""s5\"",\""color\"":\""Yellow\""},{\""id\"":\""l_8w\"",\""name\"":\""s4\"",\""color\"":\""Orange\""},{\""id\"":\""TzVT\"",\""name\"":\""s3\"",\""color\"":\""LightPink\""},{\""id\"":\""b5WF\"",\""name\"":\""s2\"",\""color\"":\""Pink\""},{\""id\"":\""AcHA\"",\""name\"":\""s1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""Hpbiwr"",""name"":""Done"",""field_type"":5,""visibility"":true,""width"":150,""type_options"":{""5"":{""is_selected"":false}},""is_primary"":false}","{""id"":""F7WLnw"",""name"":""checklist"",""field_type"":7,""visibility"":true,""width"":120,""type_options"":{""0"":{""data"":""""},""7"":{}},""is_primary"":false}","{""id"":""KABhMe"",""name"":""number"",""field_type"":1,""visibility"":true,""width"":120,""type_options"":{""1"":{""format"":0,""symbol"":""RUB"",""scale"":0,""name"":""Number""},""0"":{""scale"":0,""data"":"""",""format"":0,""name"":""Number"",""symbol"":""RUB""}},""is_primary"":false}","{""id"":""lEn6Bv"",""name"":""date"",""field_type"":2,""visibility"":true,""width"":120,""type_options"":{""2"":{""field_type"":2,""time_format"":1,""timezone_id"":"""",""date_format"":3},""0"":{""field_type"":2,""date_format"":3,""time_format"":1,""data"":"""",""timezone_id"":""""}},""is_primary"":false}","{""id"":""B8Prnx"",""name"":""url"",""field_type"":6,""visibility"":true,""width"":120,""type_options"":{""6"":{""content"":"""",""url"":""""},""0"":{""content"":"""",""data"":"""",""url"":""""}},""is_primary"":false}","{""id"":""MwUow4"",""name"":""multi-select"",""field_type"":4,""visibility"":true,""width"":240,""type_options"":{""0"":{""content"":""{\""options\"":[],\""disable_color\"":false}"",""data"":""""},""4"":{""content"":""{\""options\"":[{\""id\"":\""__Us\"",\""name\"":\""m7\"",\""color\"":\""Green\""},{\""id\"":\""n9-g\"",\""name\"":\""m6\"",\""color\"":\""Lime\""},{\""id\"":\""KFYu\"",\""name\"":\""m5\"",\""color\"":\""Yellow\""},{\""id\"":\""KftP\"",\""name\"":\""m4\"",\""color\"":\""Orange\""},{\""id\"":\""5lWo\"",\""name\"":\""m3\"",\""color\"":\""LightPink\""},{\""id\"":\""Djrz\"",\""name\"":\""m2\"",\""color\"":\""Pink\""},{\""id\"":\""2uRu\"",\""name\"":\""m1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}"
"{""field_type"":0,""created_at"":1686793246,""data"":""A"",""last_modified"":1686793246}","{""last_modified"":1686793275,""created_at"":1686793261,""data"":""AcHA"",""field_type"":3}","{""created_at"":1686793241,""field_type"":5,""last_modified"":1686793241,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""pi1A\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""6Pym\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""erEe\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""pi1A\"",\""6Pym\""]}"",""created_at"":1686793302,""field_type"":7,""last_modified"":1686793308}","{""created_at"":1686793333,""field_type"":1,""data"":""-1"",""last_modified"":1686793333}","{""last_modified"":1686793370,""field_type"":2,""data"":""1685583770"",""include_time"":false,""created_at"":1686793370}","{""created_at"":1686793395,""data"":""appflowy.io"",""field_type"":6,""last_modified"":1686793399,""url"":""https://appflowy.io""}","{""last_modified"":1686793446,""field_type"":4,""data"":""2uRu"",""created_at"":1686793428}"
"{""last_modified"":1686793247,""data"":""B"",""field_type"":0,""created_at"":1686793247}","{""created_at"":1686793278,""data"":""b5WF"",""field_type"":3,""last_modified"":1686793278}","{""created_at"":1686793292,""last_modified"":1686793292,""data"":""Yes"",""field_type"":5}","{""data"":""{\""options\"":[{\""id\"":\""YHDO\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""QjtW\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""K2nM\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""YHDO\""]}"",""field_type"":7,""last_modified"":1686793318,""created_at"":1686793311}","{""data"":""-2"",""last_modified"":1686793335,""created_at"":1686793335,""field_type"":1}","{""field_type"":2,""data"":""1685670174"",""include_time"":false,""created_at"":1686793374,""last_modified"":1686793374}","{""last_modified"":1686793403,""field_type"":6,""created_at"":1686793399,""url"":"""",""data"":""no url""}","{""data"":""2uRu,Djrz"",""field_type"":4,""last_modified"":1686793449,""created_at"":1686793449}"
"{""data"":""C"",""created_at"":1686793248,""last_modified"":1686793248,""field_type"":0}","{""created_at"":1686793280,""field_type"":3,""data"":""TzVT"",""last_modified"":1686793280}","{""data"":""Yes"",""last_modified"":1686793292,""field_type"":5,""created_at"":1686793292}","{""last_modified"":1686793329,""field_type"":7,""created_at"":1686793322,""data"":""{\""options\"":[{\""id\"":\""iWM1\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""WDvF\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""w3k7\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""iWM1\"",\""WDvF\"",\""w3k7\""]}""}","{""field_type"":1,""last_modified"":1686793339,""data"":""0.1"",""created_at"":1686793339}","{""last_modified"":1686793377,""data"":""1685756577"",""created_at"":1686793377,""include_time"":false,""field_type"":2}","{""created_at"":1686793403,""field_type"":6,""data"":""appflowy.io"",""last_modified"":1686793408,""url"":""https://appflowy.io""}","{""data"":""2uRu,Djrz,5lWo"",""created_at"":1686793453,""last_modified"":1686793454,""field_type"":4}"
"{""data"":""D"",""last_modified"":1686793249,""created_at"":1686793249,""field_type"":0}","{""data"":""l_8w"",""created_at"":1686793284,""last_modified"":1686793284,""field_type"":3}","{""data"":""Yes"",""created_at"":1686793293,""last_modified"":1686793293,""field_type"":5}",,"{""field_type"":1,""last_modified"":1686793341,""created_at"":1686793341,""data"":""0.2""}","{""created_at"":1686793379,""last_modified"":1686793379,""field_type"":2,""data"":""1685842979"",""include_time"":false}","{""last_modified"":1686793419,""field_type"":6,""created_at"":1686793408,""data"":""https://github.com/AppFlowy-IO/"",""url"":""https://github.com/AppFlowy-IO/""}","{""data"":""2uRu,Djrz,5lWo"",""last_modified"":1686793459,""field_type"":4,""created_at"":1686793459}"
"{""field_type"":0,""last_modified"":1686793250,""created_at"":1686793250,""data"":""E""}","{""field_type"":3,""last_modified"":1686793290,""created_at"":1686793290,""data"":""GzNa""}","{""last_modified"":1686793294,""created_at"":1686793294,""data"":""Yes"",""field_type"":5}",,"{""created_at"":1686793346,""field_type"":1,""last_modified"":1686793346,""data"":""1""}","{""last_modified"":1686793383,""data"":""1685929383"",""field_type"":2,""include_time"":false,""created_at"":1686793383}","{""field_type"":6,""url"":"""",""data"":"""",""last_modified"":1686793421,""created_at"":1686793419}","{""field_type"":4,""last_modified"":1686793465,""data"":""2uRu,Djrz,5lWo,KFYu,KftP"",""created_at"":1686793463}"
"{""field_type"":0,""created_at"":1686793251,""data"":"""",""last_modified"":1686793289}",,,,"{""data"":""2"",""field_type"":1,""created_at"":1686793347,""last_modified"":1686793347}","{""include_time"":false,""data"":""1685929385"",""last_modified"":1686793385,""field_type"":2,""created_at"":1686793385}",,
"{""created_at"":1686793254,""field_type"":0,""last_modified"":1686793288,""data"":""""}",,,,"{""created_at"":1686793351,""last_modified"":1686793351,""data"":""10"",""field_type"":1}","{""include_time"":false,""data"":""1686879792"",""field_type"":2,""created_at"":1686793392,""last_modified"":1686793392}",,
,,,,"{""last_modified"":1686793354,""created_at"":1686793354,""field_type"":1,""data"":""11""}",,,
,,,,"{""field_type"":1,""last_modified"":1686793356,""data"":""12"",""created_at"":1686793356}",,,
,,,,,,,
@@ -0,0 +1,22 @@
# Welcome to AppFlowy!
## Here are the basics
- [ ] Click anywhere and just start typing.
- [ ] Highlight any text, and use the editing menu to _style_ **your** <u>writing</u> `however` you ~~like.~~
- [ ] As soon as you type /a menu will pop up. Select different types of content blocks you can add.
- [ ] Type `/` followed by `/bullet` or `/num` to create a list.
- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page.
- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
---
## Keyboard shortcuts, markdown, and code block
1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts)
1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown)
1. Type `/code` to insert a code block
## Have a question❓
> Click `?` at the bottom right for help and support.
@@ -0,0 +1 @@
# test
@@ -419,4 +419,4 @@
"layoutDateField": "تقويم التخطيط بواسطة"
}
}
}
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "petit"
}
@@ -51,6 +51,13 @@
"import": "Import",
"moreOptions": "More options"
},
"importPanel": {
"textAndMarkdown": "Text & Markdown",
"documentFromV010": "Document from v0.1.0",
"databaseFromV010": "Database from v0.1.0",
"csv": "CSV",
"database": "Database"
},
"disclosureAction": {
"rename": "Rename",
"delete": "Delete",
@@ -83,7 +90,8 @@
"name": "Debug Info",
"success": "Copied debug info to clipboard!",
"fail": "Unable to copy debug info to clipboard"
}
},
"feedback": "Feedback"
},
"menuAppHeader": {
"addPageTooltip": "Quickly add a page inside",
@@ -178,13 +186,29 @@
"open": "Open Settings"
},
"appearance": {
"fontFamily": {
"label": "Font Family",
"search": "Search"
},
"themeMode": {
"label": "Theme Mode",
"light": "Light Mode",
"dark": "Dark Mode",
"system": "Adapt to System"
},
"theme": "Theme"
"themeUpload": {
"button": "Upload",
"description": "Upload your own AppFlowy theme using the button below.",
"failure": "The theme that was uploaded had an invalid format.",
"loading": "Please wait while we validate and upload your theme...",
"uploadSuccess": "Your theme was uploaded successfully",
"deletionFailure": "Failed to delete the theme. Try to delete it manually.",
"filePickerDialogTitle": "Choose a .flowy_plugin file",
"urlUploadFailure": "Failed to open url: {}"
},
"theme": "Theme",
"builtInsLabel": "Built-in Themes",
"pluginsLabel": "Plugins"
},
"files": {
"copy": "Copy",
@@ -216,6 +240,7 @@
"changeLocationTooltips": "Change the data directory",
"change": "Change",
"openLocationTooltips": "Open another data directory",
"openCurrentDataFolder": "Open current data directory",
"recoverLocationTooltips": "Reset to AppFlowy's default data directory",
"exportFileSuccess": "Export file successfully!",
"exportFileFail": "Export file failed!",
@@ -229,6 +254,8 @@
}
},
"grid": {
"deleteView": "Are you sure you want to delete this view?",
"createView": "New",
"settings": {
"filter": "Filter",
"sort": "Sort",
@@ -368,11 +395,19 @@
"grid": {
"selectAGridToLinkTo": "Select a Grid to link to",
"createANewGrid": "Create a new Grid"
},
"calendar": {
"selectACalendarToLinkTo": "Select a Calendar to link to",
"createANewCalendar": "Create a new Calendar"
}
},
"selectionMenu": {
"outline": "Outline"
},
"plugins": {
"referencedBoard": "Referenced Board",
"referencedGrid": "Referenced Grid",
"referencedCalendar": "Referenced Calendar",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
"autoGeneratorLearnMore": "Learn more",
@@ -391,6 +426,7 @@
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
"discardResponse": "Do you want to discard the AI responses?",
"createInlineMathEquation": "Create equation",
"cover": {
"changeCover": "Change Cover",
"colors": "Colors",
@@ -433,6 +469,12 @@
"center": "Center",
"right": "Right",
"defaultColor": "Default"
},
"image": {
"copiedToPasteBoard": "The image link has been copied to the clipboard"
},
"outline": {
"addHeadingToCreateOutline": "Add headings to create a table of contents."
}
}
},
@@ -459,7 +501,14 @@
"layoutDateField": "Layout calendar by",
"noDateTitle": "No Date",
"noDateHint": "Unscheduled events will show up here",
"clickToAdd": "Click to add to the calendar"
}
"clickToAdd": "Click to add to the calendar",
"name": "Calendar layout"
},
"referencedCalendarPrefix": "View of"
},
"errorDialog": {
"title": "AppFlowy Error",
"howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.",
"github": "View on GitHub"
}
}
}
@@ -227,5 +227,6 @@
"column": {
"create_new_card": "Nuevo"
}
}
},
"moreAction.small": "pequeño"
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "petit"
}
@@ -235,5 +235,6 @@
"column": {
"create_new_card": "Nouveau"
}
}
},
"moreAction.small": "petit"
}
@@ -120,14 +120,14 @@
"oAuth": {
"err": {
"failedTitle": "Sikertelen bejelentkezés.",
"failedMsg": "Kérjük győződj meg róla, hogy elvégezted a bejelentkezési folyamatot a böngésződben"
"failedMsg": "Kérjük győződj meg róla, hogy elvégezted a bejelentkezési folyamatot a böngésződben!"
},
"google": {
"title": "Bejelentkezés Google-al",
"title": "Bejelentkezés Google-lal",
"instruction1": "Ahhoz, hogy hozzáférj a Google Kontaktjaidhoz, kérjük hatalmazd fel ezt az alkalmazást a böngésződben.",
"instruction2": "Másold ezt a kódot a vágólapra az ikonra kattintással vagy a szöveg kijelölésével:",
"instruction3": "Nyisd meg ezt a linket a böngésződben, és írjd be a fenti kódot:",
"instruction4": "Nyomd meg az alábbi gombot, ha elvégezted a registrációt:"
"instruction3": "Nyisd meg ezt a linket a böngésződben, és írd be a fenti kódot:",
"instruction4": "Nyomd meg az alábbi gombot, ha elvégezted a regisztrációt:"
}
},
"settings": {
@@ -139,15 +139,17 @@
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"label": "Téma",
"light": "Világos mód",
"dark": "Éjjeli mód",
"system": "Adapt to System"
"system": "Rendszerbeállítás követése"
}
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}
},
"moreAction.small": "kicsi"
}
@@ -222,5 +222,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "kecil"
}
@@ -155,5 +155,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "piccolo"
}
@@ -203,5 +203,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "小さい"
}
@@ -231,5 +231,6 @@
"column": {
"create_new_card": "추가"
}
}
},
"moreAction.small": "작은"
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "mały"
}
@@ -145,5 +145,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "pequeno"
}
@@ -45,7 +45,14 @@
"medium": "средний",
"large": "большой",
"fontSize": "Размер шрифта",
"import": "Импортировать"
"import": "Импортировать",
"moreOptions": ""
},
"importPanel": {
"textAndMarkdown": "Текст и Markdown",
"documentFromV010": "Документ из v0.1.0",
"databaseFromV010": "База данных из v0.1.0",
"csv": "CSV"
},
"disclosureAction": {
"rename": "Переименовать",
@@ -108,8 +115,9 @@
"openAsPage": "Открыть как страницу",
"addNewRow": "Добавить новую строку",
"openMenu": "Открыть меню",
"viewDataBase": "Просмотр базы данных",
"referencePage": "Ссылаются на {name}"
"dragRow": "Долгое нажатие для изменения порядка строк",
"viewDataBase": "Открыть базу данных",
"referencePage": "Ссылки на {name}"
},
"sideBar": {
"closeSidebar": "Закрыть боковое меню",
@@ -123,7 +131,7 @@
},
"contactsPage": {
"title": "Контакты",
"whatsHappening": "Какие события на этой неделе?",
"whatsHappening": "Что нового на этой неделе?",
"addContact": "Добавить контакт",
"editContact": "Редактировать контакт"
},
@@ -182,15 +190,20 @@
"theme": "Тема"
},
"files": {
"copy": "Копировать",
"defaultLocation": "Где сейчас хранятся ваши данные",
"exportData": "Экспорт данных",
"doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь",
"restoreLocation": "Восстановить путь AppFlowy по умолчанию",
"customizeLocation": "Открыть другую папку",
"restartApp": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.",
"exportDatabase": "Экспорт базы данных",
"selectFiles": "Выбрать файлы, которые необходимо экспортировать",
"selectAll": "Выбрать всё",
"deselectAll": "Снять выделение",
"createNewFolder": "Создать новую папку",
"createNewFolderDesc": "Указать, где хранить свои данные ...",
"defineWhereYourDataIsStored": "Указать хранилище данных",
"open": "Открыть",
"openFolder": "Открыть существующую папку",
"openFolderDesc": "Чтение и запись в существующую папку AppFlowy ...",
@@ -199,9 +212,17 @@
"locationDesc": "Выбрать имя папки данных AppFlowy",
"browser": "Обзор",
"create": "Создать",
"set": "Set",
"folderPath": "Путь к вашей папке",
"locationCannotBeEmpty": "Путь не может быть пустым",
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!"
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!",
"changeLocationTooltips": "Сменить местоположения",
"change": "Изменить",
"openLocationTooltips": "Открыть другую директорию данных",
"recoverLocationTooltips": "Сбросить к местоположению по умолчанию",
"exportFileSuccess": "Экспорт завершён!",
"exportFileFail": "Не удалость экспортировать!",
"export": "Экспорт"
},
"user": {
"name": "Имя",
@@ -221,7 +242,8 @@
"deleteFilter": "Удалить фильтр",
"filterBy": "Фильтровать по...",
"typeAValue": "Введите значение...",
"layout": "Раскладка"
"layout": "Вид",
"databaseLayout": "Вид базы данных"
},
"textFilter": {
"contains": "Содержит",
@@ -272,6 +294,8 @@
"textFieldName": "Текст",
"checkboxFieldName": "Чекбокс",
"dateFieldName": "Дата",
"updatedAtFieldName": "Последнее изменение",
"createdAtFieldName": "Создано",
"numberFieldName": "Число",
"singleSelectFieldName": "Выбор",
"multiSelectFieldName": "Выбор нескольких",
@@ -341,12 +365,16 @@
},
"slashMenu": {
"board": {
"selectABoardToLinkTo": "Выбрать доску для связи с",
"selectABoardToLinkTo": "Выбрать доску",
"createANewBoard": "Создать доску"
},
"grid": {
"selectAGridToLinkTo": "Выберите сетку для связи с",
"selectAGridToLinkTo": "Выберите сетку",
"createANewGrid": "Создать сетку"
},
"calendar": {
"selectACalendarToLinkTo": "Выбрать календарь",
"createANewCalendar": "Создать календарь"
}
},
"plugins": {
@@ -363,6 +391,8 @@
"smartEditFixSpelling": "Исправить правописание",
"warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.",
"smartEditSummarize": "Выделить суть",
"smartEditImproveWriting": "Улучшить",
"smartEditMakeLonger": "Продолжить",
"smartEditCouldNotFetchResult": "Не могу получить ответ от OpenAI",
"smartEditCouldNotFetchKey": "Не могу получить токен OpenAI",
"smartEditDisabled": "Подключить OpenAI",
@@ -394,6 +424,21 @@
"mathEquation": {
"addMathEquation": "Добавить математическое выражение",
"editMathEquation": "Редактировать математическое выражение"
},
"optionAction": {
"click": "Кликните",
"toOpenMenu": " чтобы открыть меню",
"delete": "Удалить",
"duplicate": "Дублировать",
"turnInto": "Превратить",
"moveUp": "Поднять",
"moveDown": "Опустить",
"color": "Цвет",
"align": "Выравнивание",
"left": "По левому краю",
"center": "По центру",
"right": "По правому краю",
"defaultColor": "Цвет по умолчанию"
}
}
},
@@ -417,7 +462,12 @@
"showWeekNumbers": "Показывать номера недель",
"showWeekends": "Показывать выходные",
"firstDayOfWeek": "Первый день недели",
"layoutDateField": "Вид календаря"
}
"layoutDateField": "Вид календаря",
"noDateTitle": "No Date",
"noDateHint": "Unscheduled events will show up here",
"clickToAdd": "Click to add to the calendar",
"name": "Calendar layout"
},
"referencedCalendarPrefix": "Вид"
}
}
@@ -235,5 +235,6 @@
"column": {
"create_new_card": "Nytt"
}
}
},
"moreAction.small": "små"
}
@@ -149,5 +149,6 @@
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
},
"moreAction.small": "küçük"
}
@@ -6,6 +6,8 @@
"subscribeNewsletterText": "消息订阅",
"letsGoButtonText": "开始",
"title": "标题",
"youCanAlso": "您还可以",
"and": "以及",
"signUp": {
"buttonText": "注册",
"title": "注册 @:appName 账户",
@@ -21,6 +23,7 @@
"signIn": {
"loginTitle": "登录 @:appName",
"loginButtonText": "登录",
"loginAsGuestButtonText": "开始使用",
"buttonText": "登录",
"forgotPassword": "忘记密码?",
"emailHint": "邮箱",
@@ -40,6 +43,21 @@
"markdown": "Markdown",
"copyLink": "复制链接"
},
"moreAction": {
"small": "小",
"medium": "中",
"large": "大",
"fontSize": "字号",
"import": "导入",
"moreOptions": "更多选项"
},
"importPanel": {
"textAndMarkdown": "文本 & Markdown",
"documentFromV010": "来自 v0.1.0 的文档",
"databaseFromV010": "来自 v0.1.0 的数据库",
"csv": "CSV",
"database": "数据库"
},
"disclosureAction": {
"rename": "重命名",
"delete": "删除",
@@ -54,7 +72,7 @@
"pageHeader": {
"fileName": "文件名",
"lastModified": "最近修改",
"created": "创建"
"created": "创建时间"
}
},
"deletePagePrompt": {
@@ -64,8 +82,10 @@
},
"dialogCreatePageNameHint": "页面名称",
"questionBubble": {
"shortcuts": "快捷键",
"whatsNew": "新功能?",
"help": "帮助 & 支持",
"markdown": "Markdown",
"debug": {
"name": "调试信息",
"success": "将调试信息复制到剪贴板!",
@@ -90,14 +110,18 @@
"inlineCode": "内联代码",
"quote": "块引用",
"header": "标题",
"highlight": "高亮"
"highlight": "高亮",
"color": "颜色"
},
"tooltip": {
"lightMode": "切换到亮色模式",
"darkMode": "切换到暗色模式",
"openAsPage": "作为页面打开",
"addNewRow": "增加一行",
"openMenu": "点击打开菜单"
"addNewRow": "添加一行",
"openMenu": "点击打开菜单",
"dragRow": "拖动来调整行",
"viewDataBase": "查看数据库",
"referencePage": "这个 {name} 正在被引用"
},
"sideBar": {
"openSidebar": "打开侧边栏",
@@ -117,11 +141,19 @@
},
"button": {
"OK": "确认",
"Done": "完成",
"Cancel": "取消",
"signIn": "登录",
"signOut": "登出",
"complete": "完成",
"save": "保存"
"save": "保存",
"generate": "生成",
"esc": "ESC",
"keep": "保留",
"tryAgain": "重试",
"discard": "放弃",
"replace": "替换",
"insertBelow": "下方插入"
},
"label": {
"welcome": "欢迎!",
@@ -149,37 +181,133 @@
"appearance": "外观",
"language": "语言",
"user": "用户",
"files": "文件",
"open": "打开设置"
},
"appearance": {
"themeMode": {
"label": "Theme Mode",
"label": "主题模式",
"light": "日间模式",
"dark": "夜间模式",
"system": "Adapt to System"
}
"system": "系统自适应"
},
"theme": "主题"
},
"files": {
"copy": "复制",
"defaultLocation": "读取文件和数据存储位置",
"exportData": "导出您的数据",
"doubleTapToCopy": "双击复制路径",
"restoreLocation": "恢复为 AppFlowy 默认路径",
"customizeLocation": "打开另一个文件夹",
"restartApp": "请重启 app 使设置生效",
"exportDatabase": "导出数据库",
"selectFiles": "选择需要导出的文件",
"selectAll": "全选",
"deselectAll": "取消全选",
"createNewFolder": "新建文件夹",
"createNewFolderDesc": "告诉我们您要将数据存储在何处",
"defineWhereYourDataIsStored": "定义数据存储位置",
"open": "打开",
"openFolder": "打开现有文件夹",
"openFolderDesc": "读取并将其写入您现有的 AppFlowy 文件夹",
"folderHintText": "文件夹名",
"location": "正在新建文件夹",
"locationDesc": "为您的 AppFlowy 数据文件夹选择一个名称",
"browser": "浏览",
"create": "新建",
"set": "设置",
"folderPath": "保存文件夹的路径",
"locationCannotBeEmpty": "路径不能为空",
"pathCopiedSnackbar": "文件存储路径已被复制到剪贴板!",
"changeLocationTooltips": "更改数据目录",
"change": "更改",
"openLocationTooltips": "打开另一个数据目录",
"openCurrentDataFolder": "打开当前数据目录",
"recoverLocationTooltips": "恢复为 AppFlowy 默认数据目录",
"exportFileSuccess": "导出文件成功!",
"exportFileFail": "导出文件失败!",
"export": "导出"
},
"user": {
"name": "名字",
"icon": "图标",
"selectAnIcon": "选择一个图标",
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥"
}
},
"grid": {
"deleteView": "您确定要删除这个视窗吗?",
"createView": "新建",
"settings": {
"filter": "过滤器",
"filter": "筛选",
"sort": "以……排序",
"sortBy": "排序",
"Properties": "属性",
"group": "组"
"group": "组",
"addFilter": "添加筛选",
"deleteFilter": "删除筛选",
"filterBy": "以……筛选",
"typeAValue": "请输入一个值",
"layout": "布局",
"databaseLayout": "布局"
},
"textFilter": {
"contains": "包含",
"doesNotContain": "不包含",
"endsWith": "以……结束",
"startWith": "以……开始",
"is": "等于",
"isNot": "不等于",
"isEmpty": "为空",
"isNotEmpty": "不为空",
"choicechipPrefix": {
"isNot": "不等于",
"startWith": "以……开始",
"endWith": "以……结束",
"isEmpty": "为空",
"isNotEmpty": "不为空"
}
},
"checkboxFilter": {
"isChecked": "已勾选",
"isUnchecked": "未勾选",
"choicechipPrefix": {
"is": "is"
}
},
"checklistFilter": {
"isComplete": "已完成",
"isIncomplted": "未完成"
},
"singleSelectOptionFilter": {
"is": "等于",
"isNot": "不等于",
"isEmpty": "为空",
"isNotEmpty": "不为空"
},
"multiSelectOptionFilter": {
"contains": "包含",
"doesNotContain": "不包含",
"isEmpty": "为空",
"isNotEmpty": "不为空"
},
"field": {
"hide": "隐藏",
"insertLeft": "左侧插入",
"insertRight": "右侧插入",
"duplicate": "拷贝",
"duplicate": "复制",
"delete": "删除",
"textFieldName": "文本",
"checkboxFieldName": "勾选框",
"dateFieldName": "日期",
"updatedAtFieldName": "修改时间",
"createdAtFieldName": "创建时间",
"numberFieldName": "数字",
"singleSelectFieldName": "单项选择器",
"multiSelectFieldName": "多项选择器",
"urlFieldName": "链接",
"checklistFieldName": "清单",
"numberFormat": "数字格式",
"dateFormat": "日期格式",
"includeTime": "包含时间",
@@ -187,6 +315,7 @@
"dateFormatISO": "年-月-日",
"dateFormatLocal": "月/日/年",
"dateFormatUS": "年/月/日",
"dateFormatDayMonthYear": "日/月/年",
"timeFormat": "时间格式",
"invalidTimeFormat": "时间格式错误",
"timeFormatTwelveHour": "12小时制",
@@ -195,16 +324,23 @@
"optionTitle": "标签",
"addOption": "添加标签",
"editProperty": "编辑列属性",
"newProperty": "增加一列",
"newProperty": "添加一列",
"deleteFieldPromptMessage": "确定要删除这个属性吗? "
},
"sort": {
"ascending": "升序",
"descending": "降序",
"deleteSort": "删除排序",
"addSort": "添加排序"
},
"row": {
"duplicate": "复制",
"delete": "删除",
"textPlaceholder": "空",
"copyProperty": "复制列",
"count": "数量",
"newRow": "添加一行"
"newRow": "添加一行",
"action": "执行"
},
"selectOption": {
"create": "新建",
@@ -222,19 +358,132 @@
"panelTitle": "选择或新建一个标签",
"searchOption": "搜索标签"
},
"menuName": "网格"
"checklist": {
"panelTitle": "添加项"
},
"menuName": "网格",
"referencedGridPrefix": "视图"
},
"document": {
"menuName": "文档",
"date": {
"timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00"
},
"slashMenu": {
"board": {
"selectABoardToLinkTo": "选择要链接到的看板",
"createANewBoard": "新建看板"
},
"grid": {
"selectAGridToLinkTo": "选择要链接到的网格",
"createANewGrid": "新建网格"
},
"calendar": {
"selectACalendarToLinkTo": "选择要链接到的日历",
"createANewCalendar": "新建日历"
}
},
"plugins": {
"referencedBoard": "引用的看板",
"referencedGrid": "引用的网格",
"referencedCalendar": "引用的日历",
"autoGeneratorMenuItemName": "OpenAI 创作",
"autoGeneratorTitleName": "OpenAI: 让 AI 写些什么...",
"autoGeneratorLearnMore": "学习更多",
"autoGeneratorGenerate": "生成",
"autoGeneratorHintText": "让 OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "无法获得 OpenAI 密钥",
"autoGeneratorRewrite": "重写",
"smartEdit": "AI 助手",
"openAI": "OpenAI",
"smartEditFixSpelling": "修正拼写",
"warning": "⚠️ AI 可能不准确或具有误导性.",
"smartEditSummarize": "总结",
"smartEditImproveWriting": "提高写作水平",
"smartEditMakeLonger": "丰富内容",
"smartEditCouldNotFetchResult": "无法从 OpenAI 获取到结果",
"smartEditCouldNotFetchKey": "无法获取到 OpenAI 密钥",
"smartEditDisabled": "在设置中连接 OpenAI",
"discardResponse": "您是否要放弃 AI 继续写作?",
"cover": {
"changeCover": "修改封面",
"colors": "颜色",
"images": "图像",
"clearAll": "清除所有",
"abstract": "摘要",
"addCover": "添加封面",
"addLocalImage": "添加本地图像",
"invalidImageUrl": "无效的图像网址",
"failedToAddImageToGallery": "无法将图片添加到库",
"enterImageUrl": "输入图像网址",
"add": "添加",
"back": "返回",
"saveToGallery": "保存至库",
"removeIcon": "移除图标",
"pasteImageUrl": "粘贴图片网址",
"or": "或",
"pickFromFiles": "从文件中选取",
"couldNotFetchImage": "无法获取到图像",
"imageSavingFailed": "图像保存失败",
"addIcon": "添加图标",
"coverRemoveAlert": "删除后将从封面中移除。",
"alertDialogConfirmation": "您确定您要继续吗?"
},
"mathEquation": {
"addMathEquation": "添加数学公式",
"editMathEquation": "编辑数学公式"
},
"optionAction": {
"click": "点击",
"toOpenMenu": " 来打开菜单",
"delete": "删除",
"duplicate": "复制",
"turnInto": "变成",
"moveUp": "上移",
"moveDown": "下移",
"color": "颜色",
"align": "对齐",
"left": "左",
"center": "中心",
"right": "又",
"defaultColor": "默认"
}
}
},
"board": {
"column": {
"create_new_card": "新建"
},
"menuName": "看板"
"menuName": "看板",
"referencedBoardPrefix": "视图"
},
"moreAction.small": "小的",
"calendar": {
"menuName": "日历",
"defaultNewCalendarTitle": "未命名",
"navigation": {
"today": "今天",
"jumpToday": "跳转到今天",
"previousMonth": "上一月",
"nextMonth": "下一月"
},
"settings": {
"showWeekNumbers": "显示周数",
"showWeekends": "显示周末",
"firstDayOfWeek": "一周开始于",
"layoutDateField": "以……为日历布局",
"noDateTitle": "没有日期",
"noDateHint": "计划外事件将显示在此处",
"clickToAdd": "单击以添加到日历",
"name": "日历布局"
},
"referencedCalendarPrefix": "视图"
},
"errorDialog": {
"title": "AppFlowy 错误",
"howToFixFallback": "对于给您带来的不便, 我们深表歉意! 请在我们的 GitHub 页面上提交 issue 并描述您遇到的错误。",
"github": "在 GitHub 查看"
}
}
}
@@ -35,9 +35,7 @@ void main() {
setUpAll(() async => await service.setUpAll());
setUp(() async => await service.setUp());
testWidgets(
'integration test unzips the proper workspace and loads it correctly.',
(tester) async {
testWidgets('open the board with data structure in v0.2.0', (tester) async {
await tester.initializeAppFlowy();
expect(find.byType(AppFlowyBoard), findsOneWidget);
});
@@ -1,54 +0,0 @@
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
/// Integration tests for an empty board. The [TestWorkspaceService] will load
/// a workspace from an empty board `assets/test/workspaces/board.zip` for all
/// tests.
///
/// To create another integration test with a preconfigured workspace.
/// Use the following steps.
/// 1. Create a new workspace from the AppFlowy launch screen.
/// 2. Modify the workspace until it is suitable as the starting point for
/// the integration test you need to land.
/// 3. Use a zip utility program to zip the workspace folder that you created.
/// 4. Add the zip file under `assets/test/workspaces/`
/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`.
/// For example, if you added a workspace called `empty_calendar.zip`,
/// then [TestWorkspace] should have the following value:
/// ```dart
/// enum TestWorkspace {
/// board('board'),
/// empty_calendar('empty_calendar');
///
/// /* code */
/// }
/// ```
/// 6. Double check that the .zip file that you added is included as an asset in
/// the pubspec.yaml file under appflowy_flutter.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const service = TestWorkspaceService(TestWorkspace.coverImage);
group('cover image', () {
setUpAll(() async => await service.setUpAll());
setUp(() async => await service.setUp());
testWidgets(
'hovering on cover image will display change and delete cover image buttons',
(tester) async {
await tester.initializeAppFlowy();
expect(find.byType(Image), findsOneWidget);
final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse);
final imageFinder = find.byType(Image);
final Offset offset = tester.getCenter(imageFinder);
pointer.hover(offset);
expect(find.byType(RoundedTextButton), findsOneWidget);
});
});
}
@@ -0,0 +1,189 @@
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('calendar database view', () {
testWidgets('update calendar layout', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// open setting
await tester.tapDatabaseSettingButton();
await tester.tapDatabaseLayoutButton();
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.tapDatabaseSettingButton();
await tester.tapDatabaseLayoutButton();
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid);
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid);
await tester.pumpAndSettle();
});
testWidgets('calendar start from day setting', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create calendar view
await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar');
// Open setting
await tester.tapDatabaseSettingButton();
await tester.tapCalendarLayoutSettingButton();
// select the first day of week is Monday
await tester.tapFirstDayOfWeek();
await tester.tapFirstDayOfWeekStartFromMonday();
// Open the other page and open the new calendar page again
await tester.openPage(readme);
await tester.pumpAndSettle(const Duration(milliseconds: 300));
await tester.openPage('calendar');
// Open setting again and check the start from Monday is selected
await tester.tapDatabaseSettingButton();
await tester.tapCalendarLayoutSettingButton();
await tester.tapFirstDayOfWeek();
tester.assertFirstDayOfWeekStartFromMonday();
await tester.pumpAndSettle();
});
testWidgets('creating and editing calendar events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Hover over today's calendar cell
await tester.hoverOnTodayCalendarCell();
// Tap on create new event button
await tester.tapAddCalendarEventButton();
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
tester.assertNumberOfEventsInCalendar(1);
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(2);
// Click on the event
await tester.openCalendarEvent(index: 0);
tester.assertRowDetailPageOpened();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
await tester.dismissRowDetailPage();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Click on the event
await tester.openCalendarEvent(index: 1);
tester.assertRowDetailPageOpened();
// Duplicate the event
await tester.tapRowDetailPageDuplicateRowButton();
await tester.dismissRowDetailPage();
// Check that there are 2 events
tester.assertNumberOfEventsInCalendar(2, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(3, DateTime.now());
// Delete an event
await tester.openCalendarEvent(index: 1);
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 1 event
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
});
testWidgets('rescheduling events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
// Create a new event on the first of this month
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
// Make sure that the event has been rescheduled to the new date
final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, sameDayNextWeek);
// Delete the event
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
await tester.tapRowDetailPageDeleteRowButton();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(today);
await tester.dismissRowDetailPage();
// Make sure that the event is today
tester.assertNumberOfEventsOnSpecificDay(1, today);
// Click on the event
await tester.openCalendarEvent(index: 0);
// Open the date editor of the event
await tester.tapDateCellInRowDetailPage();
await tester.findDateEditor(findsOneWidget);
// Edit the event's date. To avoid selecting a day outside of the current month, the new date will be one day closer to the middle of the month.
final newDate = today.day < 15
? today.add(const Duration(days: 1))
: today.subtract(const Duration(days: 1));
await tester.selectDay(content: newDate.day);
await tester.dismissCellEditor();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, newDate);
});
});
}
@@ -0,0 +1,437 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid cell', () {
testWidgets('edit text cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'hello world',
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'hello world',
);
await tester.pumpAndSettle();
});
// Makesure the text cells are filled with the right content when there are
// multiple text cell
testWidgets('edit multiple text cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid');
await tester.createField(FieldType.RichText, 'description');
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'hello',
);
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.RichText,
input: 'world',
cellIndex: 1,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'hello',
cellIndex: 0,
);
await tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.RichText,
content: 'world',
cellIndex: 1,
);
await tester.pumpAndSettle();
});
testWidgets('edit number cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
const fieldType = FieldType.Number;
// Create a number field
await tester.createField(fieldType, fieldType.name);
await tester.editCell(
rowIndex: 0,
fieldType: fieldType,
input: '-1',
);
// edit the next cell to force the previous cell at row 0 to lose focus
await tester.editCell(
rowIndex: 1,
fieldType: fieldType,
input: '0.2',
);
// -1 -> -1
await tester.assertCellContent(
rowIndex: 0,
fieldType: fieldType,
content: '-1',
);
// edit the next cell to force the previous cell at row 1 to lose focus
await tester.editCell(
rowIndex: 2,
fieldType: fieldType,
input: '.1',
);
// 0.2 -> 0.2
await tester.assertCellContent(
rowIndex: 1,
fieldType: fieldType,
content: '0.2',
);
// edit the next cell to force the previous cell at row 2 to lose focus
await tester.editCell(
rowIndex: 0,
fieldType: fieldType,
input: '',
);
// .1 -> 0.1
await tester.assertCellContent(
rowIndex: 2,
fieldType: fieldType,
content: '0.1',
);
await tester.pumpAndSettle();
});
testWidgets('edit checkbox cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.assertCheckboxCell(rowIndex: 0, isSelected: false);
await tester.tapCheckboxCellInGrid(rowIndex: 0);
await tester.assertCheckboxCell(rowIndex: 0, isSelected: true);
await tester.tapCheckboxCellInGrid(rowIndex: 1);
await tester.tapCheckboxCellInGrid(rowIndex: 2);
await tester.assertCheckboxCell(rowIndex: 1, isSelected: true);
await tester.assertCheckboxCell(rowIndex: 2, isSelected: true);
await tester.pumpAndSettle();
});
testWidgets('edit create time cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
const fieldType = FieldType.CreatedTime;
// Create a create time field
// The create time field is not editable
await tester.createField(fieldType, fieldType.name);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsNothing);
await tester.pumpAndSettle();
});
testWidgets('edit last time cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
const fieldType = FieldType.LastEditedTime;
// Create a last time field
// The last time field is not editable
await tester.createField(fieldType, fieldType.name);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsNothing);
await tester.pumpAndSettle();
});
testWidgets('edit time cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
const fieldType = FieldType.DateTime;
await tester.createField(fieldType, fieldType.name);
// Tap the cell to invoke the field editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Toggle include time
await tester.toggleIncludeTime();
// Dismiss the cell editor
await tester.dismissCellEditor();
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Turn off include time
await tester.toggleIncludeTime();
// Select a date
final today = DateTime.now();
await tester.selectDay(content: today.day);
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM dd, y').format(today),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Toggle include time
final now = DateTime.now();
await tester.toggleIncludeTime();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('MMM dd, y HH:mm').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Change date format
await tester.changeDateFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y HH:mm').format(now),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Change time format
await tester.changeTimeFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
rowIndex: 0,
fieldType: fieldType,
content: DateFormat('dd/MM/y hh:mm a').format(now),
);
await tester.pumpAndSettle();
});
testWidgets('edit single select cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
const fieldType = FieldType.SingleSelect;
await tester.tapAddButton();
// When create a grid, it will create a single select field by default
await tester.tapCreateGridButton();
// Tap the cell to invoke the selection option editor
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create a new select option
await tester.createOption(name: 'tag 1');
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 1',
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create another select option
await tester.createOption(name: 'tag 2');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 2',
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// switch to first option
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: 'tag 1',
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsOneWidget,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Deselect the currently-selected option
await tester.selectOption(name: 'tag 1');
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
await tester.pumpAndSettle();
});
testWidgets('edit multi select cell', (tester) async {
final tags = [
'tag 1',
'tag 2',
'tag 3',
'tag 4',
];
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
const fieldType = FieldType.MultiSelect;
await tester.createField(fieldType, fieldType.name);
// Tap the cell to invoke the selection option editor
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create a new select option
await tester.createOption(name: tags.first);
await tester.dismissCellEditor();
// Make sure the option is created and displayed in the cell
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags.first,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Create some other select options
await tester.createOption(name: tags[1]);
await tester.createOption(name: tags[2]);
await tester.createOption(name: tags[3]);
await tester.dismissCellEditor();
for (final tag in tags) {
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tag,
);
}
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(4),
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Deselect all options
for (final tag in tags) {
await tester.selectOption(name: tag);
}
await tester.dismissCellEditor();
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNothing,
);
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findSelectOptionEditor(findsOneWidget);
// Select some options
await tester.selectOption(name: tags[1]);
await tester.selectOption(name: tags[3]);
await tester.dismissCellEditor();
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[1],
);
await tester.findSelectOptionWithNameInGrid(
rowIndex: 0,
name: tags[3],
);
await tester.assertNumberOfSelectedOptionsInGrid(
rowIndex: 0,
matcher: findsNWidgets(2),
);
await tester.pumpAndSettle();
});
});
}
@@ -0,0 +1,186 @@
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid page', () {
testWidgets('rename existing field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Invoke the field editor
await tester.tapGridFieldWithName('Name');
await tester.tapEditPropertyButton();
await tester.renameField('hello world');
await tester.dismissFieldEditor();
await tester.tapGridFieldWithName('hello world');
await tester.pumpAndSettle();
});
testWidgets('update field type of existing field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
await tester.tapTypeOptionButton();
await tester.selectFieldType(FieldType.Checkbox);
await tester.dismissFieldEditor();
await tester.assertFieldTypeWithFieldName(
'Type',
FieldType.Checkbox,
);
await tester.pumpAndSettle();
});
testWidgets('create a field and rename it', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// create a field
await tester.createField(FieldType.Checklist, 'checklist');
// check the field is created successfully
await tester.findFieldWithName('checklist');
await tester.pumpAndSettle();
});
testWidgets('delete field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// create a field
await tester.createField(FieldType.Checkbox, 'New field 1');
// Delete the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapDeletePropertyButton();
// confirm delete
await tester.tapDialogOkButton();
await tester.noFieldWithName('New field 1');
await tester.pumpAndSettle();
});
testWidgets('duplicate field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// Delete the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapDuplicatePropertyButton();
await tester.findFieldWithName('New field 1 (copy)');
await tester.pumpAndSettle();
});
testWidgets('hide field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// Delete the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapHidePropertyButton();
await tester.noFieldWithName('New field 1');
await tester.pumpAndSettle();
});
testWidgets('create checklist field ', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
// Open the type option menu
await tester.tapTypeOptionButton();
await tester.selectFieldType(FieldType.Checklist);
// After update the field type, the cells should be updated
await tester.findCellByFieldType(FieldType.Checklist);
await tester.pumpAndSettle();
});
testWidgets('create list of fields', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
for (final fieldType in [
FieldType.Checklist,
FieldType.DateTime,
FieldType.Number,
FieldType.URL,
FieldType.MultiSelect,
FieldType.LastEditedTime,
FieldType.CreatedTime,
FieldType.Checkbox,
]) {
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField(fieldType.name);
// Open the type option menu
await tester.tapTypeOptionButton();
await tester.selectFieldType(fieldType);
await tester.dismissFieldEditor();
// After update the field type, the cells should be updated
await tester.findCellByFieldType(fieldType);
await tester.pumpAndSettle();
}
});
});
}
@@ -0,0 +1,142 @@
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('add text filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name');
await tester.tapFilterButtonInGrid('Name');
// enter 'A' in the filter text field
await tester.assertNumberOfRowsInGridPage(10);
await tester.enterTextInTextFilter('A');
await tester.assertNumberOfRowsInGridPage(1);
// after remove the filter, the grid should show all rows
await tester.enterTextInTextFilter('');
await tester.assertNumberOfRowsInGridPage(10);
await tester.enterTextInTextFilter('B');
await tester.assertNumberOfRowsInGridPage(1);
// open the menu to delete the filter
await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
await tester.assertNumberOfRowsInGridPage(10);
await tester.pumpAndSettle();
});
testWidgets('add checkbox filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');
await tester.assertNumberOfRowsInGridPage(5);
await tester.tapFilterButtonInGrid('Done');
await tester.tapCheckboxFilterButtonInGrid();
await tester.tapUnCheckedButtonOnCheckboxFilter();
await tester.assertNumberOfRowsInGridPage(5);
await tester
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
await tester.tapDeleteFilterButtonInGrid();
await tester.assertNumberOfRowsInGridPage(10);
await tester.pumpAndSettle();
});
testWidgets('add checklist filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');
// By default, the condition of checklist filter is 'uncompleted'
await tester.assertNumberOfRowsInGridPage(9);
await tester.tapFilterButtonInGrid('checklist');
await tester.tapChecklistFilterButtonInGrid();
await tester.tapCompletedButtonOnChecklistFilter();
await tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
testWidgets('add single select filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type');
await tester.tapFilterButtonInGrid('Type');
// select the option 's6'
await tester.tapOptionFilterWithName('s6');
await tester.assertNumberOfRowsInGridPage(0);
// unselect the option 's6'
await tester.tapOptionFilterWithName('s6');
await tester.assertNumberOfRowsInGridPage(10);
// select the option 's5'
await tester.tapOptionFilterWithName('s5');
await tester.assertNumberOfRowsInGridPage(1);
// select the option 's4'
await tester.tapOptionFilterWithName('s4');
// The row with 's4' or 's5' should be shown.
await tester.assertNumberOfRowsInGridPage(2);
await tester.pumpAndSettle();
});
testWidgets('add multi select filter', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.MultiSelect,
'multi-select',
);
await tester.tapFilterButtonInGrid('multi-select');
await tester.scrollOptionFilterListByOffset(const Offset(0, -200));
// select the option 'm1'. Any option with 'm1' should be shown.
await tester.tapOptionFilterWithName('m1');
await tester.assertNumberOfRowsInGridPage(5);
await tester.tapOptionFilterWithName('m1');
// select the option 'm2'. Any option with 'm2' should be shown.
await tester.tapOptionFilterWithName('m2');
await tester.assertNumberOfRowsInGridPage(4);
await tester.tapOptionFilterWithName('m2');
// select the option 'm4'. Any option with 'm4' should be shown.
await tester.tapOptionFilterWithName('m4');
await tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
});
}
@@ -0,0 +1,231 @@
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/emoji.dart';
import 'util/ime.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('row details page opens', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Make sure that the row page is opened
tester.assertRowDetailPageOpened();
});
testWidgets('insert emoji in the row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// After select the emoji, the EmojiButton will show up
await tester.tapButton(find.byType(EmojiButton));
});
testWidgets('update emoji in the row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// Update existing selected emoji
await tester.tapButton(find.byType(EmojiButton));
await tester.switchToEmojiList();
await tester.tapEmoji('😅');
// The emoji already displayed in the row banner
final emojiText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == '😅',
);
// The number of emoji should be two. One in the row displayed in the grid
// one in the row detail page.
expect(emojiText, findsNWidgets(2));
});
testWidgets('remove emoji in the row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// Remove the emoji
await tester.tapButton(find.byType(RemoveEmojiButton));
final emojiText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == '😀',
);
expect(emojiText, findsNothing);
});
testWidgets('create list of fields in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
for (final fieldType in [
FieldType.Checklist,
FieldType.DateTime,
FieldType.Number,
FieldType.URL,
FieldType.MultiSelect,
FieldType.LastEditedTime,
FieldType.CreatedTime,
FieldType.Checkbox,
]) {
await tester.tapRowDetailPageCreatePropertyButton();
await tester.renameField(fieldType.name);
// Open the type option menu
await tester.tapTypeOptionButton();
await tester.selectFieldType(fieldType);
await tester.dismissFieldEditor();
// After update the field type, the cells should be updated
await tester.findCellByFieldType(fieldType);
await tester.scrollRowDetailByOffset(const Offset(0, -50));
}
});
testWidgets('check document is exist in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Each row detail page should have a document
await tester.assertDocumentExistInRowDetailPage();
});
testWidgets('update the content of the document and re-open it',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Wait for the document to be loaded
await tester.wait(500);
// Focus on the editor
final textBlock = find.byType(TextBlockComponentWidget);
await tester.tapAt(tester.getCenter(textBlock));
await tester.pumpAndSettle();
// Input some text
const inputText = 'Hello World';
await tester.ime.insertText(inputText);
expect(
find.textContaining(inputText, findRichText: true),
findsOneWidget,
);
// Tap outside to dismiss the field
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
// Re-open the document
await tester.openFirstRowDetailPage();
expect(
find.textContaining(inputText, findRichText: true),
findsOneWidget,
);
});
testWidgets('delete row in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.tapRowDetailPageDeleteRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(2);
});
testWidgets('duplicate row in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.tapRowDetailPageDuplicateRowButton();
await tester.tapEscButton();
await tester.assertNumberOfRowsInGridPage(4);
});
});
}
@@ -0,0 +1,70 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('create row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.tapCreateRowButtonInGrid();
// The initial number of rows is 3
await tester.assertNumberOfRowsInGridPage(4);
await tester.pumpAndSettle();
});
testWidgets('create row from row menu of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonInRowMenuOfGrid();
// The initial number of rows is 3
await tester.assertNumberOfRowsInGridPage(4);
await tester.assertRowCountInGridPage(4);
await tester.pumpAndSettle();
});
testWidgets('delete row of the grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.hoverOnFirstRowOfGrid();
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu();
// The initial number of rows is 3
await tester.assertNumberOfRowsInGridPage(2);
await tester.assertRowCountInGridPage(2);
await tester.pumpAndSettle();
});
testWidgets('check number of row indicator in the initial grid',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.assertRowCountInGridPage(3);
await tester.pumpAndSettle();
});
});
}
@@ -0,0 +1,51 @@
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('update layout', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// open setting
await tester.tapDatabaseSettingButton();
// select the layout
await tester.tapDatabaseLayoutButton();
// select layout by board
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.pumpAndSettle();
});
testWidgets('update layout multiple times', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// open setting
await tester.tapDatabaseSettingButton();
await tester.tapDatabaseLayoutButton();
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
await tester.tapDatabaseSettingButton();
await tester.tapDatabaseLayoutButton();
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Calendar);
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar);
await tester.pumpAndSettle();
});
});
}
@@ -0,0 +1,166 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
testWidgets('import v0.2.0 database data', (tester) async {
await tester.openV020database();
// wait the database data is loaded
await tester.pumpAndSettle(const Duration(microseconds: 500));
// check the text cell
final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];
for (final (index, content) in textCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
// check the checkbox cell
final checkboxCells = <bool>[
true,
true,
true,
true,
true,
false,
false,
false,
false,
false
];
for (final (index, content) in checkboxCells.indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// check the number cell
final numberCells = <String>[
'-1',
'-2',
'0.1',
'0.2',
'1',
'2',
'10',
'11',
'12',
''
];
for (final (index, content) in numberCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// check the url cell
final urlCells = <String>[
'appflowy.io',
'no url',
'appflowy.io',
'https://github.com/AppFlowy-IO/',
'',
'',
];
for (final (index, content) in urlCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.URL,
content: content,
);
}
// check the single select cell
final singleSelectCells = <String>[
's1',
's2',
's3',
's4',
's5',
'',
'',
'',
'',
'',
];
for (final (index, content) in singleSelectCells.indexed) {
await tester.assertSingleSelectOption(
rowIndex: index,
content: content,
);
}
// check the multi select cell
final List<List<String>> multiSelectCells = [
['m1'],
['m1', 'm2'],
['m1', 'm2', 'm3'],
['m1', 'm2', 'm3'],
['m1', 'm2', 'm3', 'm4', 'm5'],
[],
[],
[],
[],
[],
];
for (final (index, contents) in multiSelectCells.indexed) {
await tester.assertMultiSelectOption(
rowIndex: index,
contents: contents,
);
}
// check the checklist cell
final List<double> checklistCells = [
0.6,
0.3,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
];
for (final (index, percent) in checklistCells.indexed) {
await tester.assertChecklistCellInGrid(
rowIndex: index,
percent: percent,
);
}
// check the date cell
final List<String> dateCells = [
'Jun 01, 2023',
'Jun 02, 2023',
'Jun 03, 2023',
'Jun 04, 2023',
'Jun 05, 2023',
'Jun 05, 2023',
'Jun 16, 2023',
'',
'',
''
];
for (final (index, content) in dateCells.indexed) {
await tester.assertDateCellInGrid(
rowIndex: index,
fieldType: FieldType.DateTime,
content: content,
);
}
});
});
}
@@ -0,0 +1,267 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('add text sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
// check the text cell order
final textCells = <String>[
'',
'',
'',
'',
'',
'A',
'B',
'C',
'D',
'E',
];
for (final (index, content) in textCells.indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Name');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'E',
'D',
'C',
'B',
'A',
'',
'',
'',
'',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
// delete all sorts
await tester.tapSortMenuInSettingBar();
await tester.tapAllSortButton();
// check the text cell order
for (final (index, content) in <String>[
'A',
'B',
'C',
'D',
'E',
'',
'',
'',
'',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
await tester.pumpAndSettle();
});
testWidgets('add checkbox sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
// check the checkbox cell order
for (final (index, content) in <bool>[
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Done');
await tester.tapSortByDescending();
for (final (index, content) in <bool>[
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
await tester.pumpAndSettle();
});
testWidgets('add number sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
// check the number cell order
for (final (index, content) in <String>[
'',
'-2',
'-1',
'0.1',
'0.2',
'1',
'2',
'10',
'11',
'12',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// open the sort menu and select order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('number');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'1',
'0.2',
'0.1',
'-1',
'-2',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
await tester.pumpAndSettle();
});
testWidgets('add number and text sort', (tester) async {
await tester.openV020database();
// create a filter
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
// open the sort menu and select number order by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('number');
await tester.tapSortByDescending();
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'1',
'0.2',
'0.1',
'-1',
'-2',
'',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
await tester.tapSortMenuInSettingBar();
await tester.tapCreateSortByFieldTypeInSortMenu(
FieldType.RichText,
'Name',
);
// check number cell order
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'',
'-1',
'-2',
'0.1',
'0.2',
'1',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// check text cell order
for (final (index, content) in <String>[
'',
'',
'',
'',
'',
'A',
'B',
'C',
'D',
'E',
].indexed) {
await tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.RichText,
content: content,
);
}
await tester.pumpAndSettle();
});
});
}
@@ -0,0 +1,81 @@
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database', () {
testWidgets('create linked view', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// Create grid view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
// Create calendar view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
await tester.pumpAndSettle();
});
testWidgets('rename and delete linked view', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// rename board view
await tester.renameLinkedView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
'new board',
);
final findBoard = tester.findTabBarLinkViewByViewName('new board');
expect(findBoard, findsOneWidget);
// delete the board
await tester.deleteDatebaseView(findBoard);
expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing);
await tester.pumpAndSettle();
});
testWidgets('delete the last database view', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// delete the board
await tester.deleteDatebaseView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
);
await tester.pumpAndSettle();
});
});
}
@@ -4,27 +4,12 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
group('create and delete the document', () {
testWidgets('create a new document when launching app in first time',
(tester) async {
await tester.initializeAppFlowy();
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'document_create_and_delete_test.dart'
as document_create_and_delete_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
import 'document_with_database_test.dart' as document_with_database_test;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'edit_document_test.dart' as document_edit_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_create_and_delete_test.main();
document_edit_test.main();
document_with_database_test.main();
document_with_inline_page_test.main();
document_with_inline_math_equation_test.main();
document_with_cover_image_test.main();
}
@@ -0,0 +1,120 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/emoji.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('cover image', () {
testWidgets('document cover tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// 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 solid color background
await tester.editor.tapOnChangeCover();
await tester.editor.switchSolidColorBackground();
await tester.editor.dismissCoverPicker();
tester.expectToSeeDocumentCover(CoverType.color);
// Change cover to a network image
const imageUrl =
"https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy/main/frontend/appflowy_flutter/assets/images/appflowy_launch_splash.jpg";
await tester.editor.hoverOnCover();
await tester.editor.tapOnChangeCover();
await tester.editor.addNetworkImageCover(imageUrl);
await tester.editor.switchNetworkImageCover(imageUrl);
await tester.editor.dismissCoverPicker();
tester.expectToSeeDocumentCover(CoverType.file);
// Remove the cover
await tester.editor.hoverOnCover();
await tester.editor.tapOnRemoveCover();
tester.expectToSeeNoDocumentCover();
});
testWidgets('document icon tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
// Remove the document icon from the cover toolbar
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapRemoveIconButton();
tester.expectToSeeDocumentIcon(null);
// Add the icon back for further testing
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
// Change the document icon
await tester.editor.tapOnIconWidget();
await tester.switchToEmojiList();
await tester.tapEmoji('😅');
tester.expectToSeeDocumentIcon('😅');
// Remove the document icon from the icon picker
await tester.editor.tapOnIconWidget();
await tester.editor.tapRemoveIconButton(isInPicker: true);
tester.expectToSeeDocumentIcon(null);
});
testWidgets('icon and cover at the same time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// Insert a document cover
await tester.editor.tapOnAddCover();
// Expect to see the icon and cover at the same time
tester.expectToSeeDocumentIcon('😀');
tester.expectToSeeDocumentCover(CoverType.asset);
// Hover over the cover toolbar and see that neither icons are shown
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeeEmptyDocumentHeaderToolbar();
});
});
}
@@ -0,0 +1,100 @@
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database view in document', () {
testWidgets('insert a referenced grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertReferenceDatabase(tester, ViewLayoutPB.Grid);
// validate the referenced grid is inserted
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(GridPage),
),
findsOneWidget,
);
});
testWidgets('insert a referenced board', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertReferenceDatabase(tester, ViewLayoutPB.Board);
// validate the referenced board is inserted
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(BoardPage),
),
findsOneWidget,
);
});
testWidgets('insert a referenced calendar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
// validate the referenced grid is inserted
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(CalendarPage),
),
findsOneWidget,
);
});
});
}
/// Insert a referenced database of [layout] into the document
Future<void> insertReferenceDatabase(
WidgetTester tester,
ViewLayoutPB layout,
) async {
// create a new grid
final id = uuid();
final name = '${layout.name}_$id';
await tester.createNewPageWithName(
layout,
name,
);
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'insert_a_reference_${layout.name}',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a referenced view
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
layout.referencedMenuName,
);
final linkToPageMenu = find.byType(LinkToPageMenu);
expect(linkToPageMenu, findsOneWidget);
final referencedDatabase = find.descendant(
of: linkToPageMenu,
matching: find.findTextInFlowyText(name),
);
expect(referencedDatabase, findsOneWidget);
await tester.tapButton(referencedDatabase);
}
@@ -0,0 +1,121 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/ime.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
group('inline math equation in document', () {
testWidgets('insert an inline math equation', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
// 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.byTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block
final inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsOneWidget);
// tap it and update the content
await tester.tapButton(inlineMathEquation);
final textFormField = find.descendant(
of: find.byType(MathInputTextField),
matching: find.byType(TextFormField),
);
const newFormula = 'E = MC ^ 3';
await tester.enterText(textFormField, newFormula);
await tester.tapButton(
find.descendant(
of: find.byType(MathInputTextField),
matching: find.byType(FlowyButton),
),
);
await tester.pumpAndSettle();
});
testWidgets('remove the inline math equation format', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
// 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
var inlineMathEquationButton = find.byTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block
var inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsOneWidget);
// highlight the math equation block
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: 1),
);
// expect to the see the inline math equation button is highlighted
inlineMathEquationButton = find.byWidgetPredicate(
(widget) =>
widget is IconItemWidget &&
widget.tooltip ==
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
expect(
tester.widget<IconItemWidget>(inlineMathEquationButton).isHighlight,
isTrue,
);
// cancel the format
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block is removed
inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsNothing);
tester.expectToSeeText(formula);
});
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -0,0 +1,118 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('inline page view in document', () {
testWidgets('insert a inline page - grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Grid);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - board', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Board);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - calendar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Calendar);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Document);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page and rename it', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document);
// rename
await tester.hoverOnPageName(pageName);
const newName = 'RenameToNewPageName';
await tester.renamePage(newName);
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(newName),
);
expect(finder, findsOneWidget);
});
testWidgets('insert a inline page and delete it', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid);
// rename
await tester.hoverOnPageName(pageName);
await tester.tapDeletePageButton();
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(pageName),
);
expect(finder, findsOneWidget);
await tester.tapButton(finder);
expect(find.byType(FlowyErrorPage), findsOneWidget);
});
});
}
/// Insert a referenced database of [layout] into the document
Future<String> insertingInlinePage(
WidgetTester tester,
ViewLayoutPB layout,
) async {
// create a new grid
final id = uuid();
final name = '${layout.name}_$id';
await tester.createNewPageWithName(
layout,
name,
);
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'insert_a_inline_page_${layout.name}',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
await tester.editor.showAtMenu();
await tester.editor.tapAtMenuItemWithName(name);
return name;
}
@@ -0,0 +1,130 @@
import 'dart:io';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/ime.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('edit document', () {
testWidgets('redo & undo', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document called Sample
const pageName = 'Sample';
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
// insert 1. to trigger it to be a numbered list
await tester.ime.insertText('1. ');
expect(find.text('1.', findRichText: true), findsOneWidget);
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
NumberedListBlockKeys.type,
);
// undo
// numbered list will be reverted to paragraph
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
ParagraphBlockKeys.type,
);
// redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isWindows || Platform.isLinux,
isMetaPressed: Platform.isMacOS,
isShiftPressed: true,
);
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
NumberedListBlockKeys.type,
);
// switch to other page and switch back
await tester.openPage(readme);
await tester.openPage(pageName);
// the numbered list should be kept
expect(
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
NumberedListBlockKeys.type,
);
});
testWidgets('write a readme document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new document called Sample
const pageName = 'Sample';
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
// mock inputting the sample
final lines = _sample.split('\n');
for (final line in lines) {
await tester.ime.insertText(line);
await tester.ime.insertCharacter('\n');
}
// switch to other page and switch back
await tester.openPage(readme);
await tester.openPage(pageName);
// this screenshots are different on different platform, so comment it out temporarily.
// check the document
// await expectLater(
// find.byType(AppFlowyEditor),
// matchesGoldenFile('document/edit_document_test.png'),
// );
});
});
}
// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet.
const _sample = r'''
# Heading 1
## Heading 2
### Heading 3
---
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
[] Type followed by bullet or num to create a list.
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
---
* bulleted list 1
* bulleted list 2
* bulleted list 3
bulleted list 4
---
1. numbered list 1
2. numbered list 2
3. numbered list 3
numbered list 4
---
" quote''';
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

@@ -0,0 +1,95 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.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 'util/keyboard.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('hotkeys test', () {
testWidgets('toggle theme mode', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.appearance);
await tester.pumpAndSettle();
tester.expectToSeeText(
LocaleKeys.settings_appearance_themeMode_system.tr(),
);
await tester.tapButton(
find.bySemanticsLabel(
LocaleKeys.settings_appearance_themeMode_system.tr(),
),
);
await tester.pumpAndSettle();
await tester.tapButton(
find.bySemanticsLabel(
LocaleKeys.settings_appearance_themeMode_dark.tr(),
),
);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.tap(find.byType(SettingsDialog));
await tester.pumpAndSettle();
await FlowyTestKeyboard.simulateKeyDownEvent(
[
Platform.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyL,
],
tester: tester,
);
await tester.pumpAndSettle();
tester.expectToSeeText(
LocaleKeys.settings_appearance_themeMode_light.tr(),
);
});
testWidgets('show or hide home menu', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.pumpAndSettle();
expect(find.byType(HomeMenu), findsOneWidget);
await FlowyTestKeyboard.simulateKeyDownEvent(
[
Platform.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control,
LogicalKeyboardKey.backslash,
],
tester: tester,
);
await tester.pumpAndSettle();
expect(find.byType(HomeMenu), findsNothing);
});
});
}
@@ -0,0 +1,48 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
import 'package:path/path.dart' as p;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('import files', () {
testWidgets('import multiple markdown files', (tester) async {
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
await tester.tapAddButton();
await tester.tapImportButton();
final testFileNames = ['test1.md', 'test2.md'];
final paths = <String>[];
for (final fileName in testFileNames) {
final str = await rootBundle.loadString(
'assets/test/workspaces/markdowns/$fileName',
);
final path = p.join(context.applicationDataDirectory, fileName);
paths.add(path);
File(path).writeAsStringSync(str);
}
// mock get files
await mockPickFilePaths(
paths: testFileNames
.map((e) => p.join(context.applicationDataDirectory, e))
.toList(),
);
await tester.tapTextAndMarkdownButton();
tester.expectToSeePageName('test1');
tester.expectToSeePageName('test2');
});
});
}
@@ -0,0 +1,35 @@
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document', () {
testWidgets(
'change the language successfully when launching the app for the first time',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapLanguageSelectorOnWelcomePage();
expect(find.byType(LanguageItemsListView), findsOneWidget);
await tester.tapLanguageItem(languageCode: 'zh', countryCode: 'CN');
tester.expectToSeeText('开始');
await tester.tapLanguageItem(languageCode: 'en', scrollDelta: -100);
tester.expectToSeeText('Quick Start');
await tester.tapLanguageItem(languageCode: 'it', countryCode: 'IT');
tester.expectToSeeText('Andiamo');
});
/// Make sure this test is executed after the test above.
testWidgets('check the language after relaunching the app', (tester) async {
await tester.initializeAppFlowy();
tester.expectToSeeText('Andiamo');
});
});
}
@@ -17,7 +17,8 @@ void main() {
setUpAll(() async => await service.setUpAll());
setUp(() async => await service.setUp());
testWidgets('testing selection on open-ai smart menu replace', (tester) async {
testWidgets('testing selection on open-ai smart menu replace',
(tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
@@ -38,7 +39,8 @@ void main() {
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
await tester
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
await tester.pumpAndSettle();
expect(
@@ -49,7 +51,8 @@ void main() {
),
);
});
testWidgets('testing selection on open-ai smart menu insert', (tester) async {
testWidgets('testing selection on open-ai smart menu insert',
(tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
@@ -69,7 +72,8 @@ void main() {
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
await tester
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
await tester.pumpAndSettle();
expect(
@@ -0,0 +1,103 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/ime.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('outline block test', () {
testWidgets('insert an outline block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'outline_test',
);
await tester.editor.tapLineOfEditorAt(0);
await insertOutlineInDocument(tester);
// validate the outline is inserted
expect(find.byType(OutlineBlockWidget), findsOneWidget);
});
testWidgets('insert an outline block and check if headings are visible',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'outline_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# Heading 1\n');
await tester.ime.insertText('## Heading 2\n');
await tester.ime.insertText('### Heading 3\n');
/* Results in:
* # Heading 1
* ## Heading 2
* ### Heading 3
*/
await tester.editor.tapLineOfEditorAt(3);
await insertOutlineInDocument(tester);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 1'),
),
findsOneWidget,
);
// Heading 2 is prefixed with a bullet
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 2'),
),
findsOneWidget,
);
// Heading 3 is prefixed with a dash
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 3'),
),
findsOneWidget,
);
// update the Heading 1 to Heading 1Hello world
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('Hello world');
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 1Hello world'),
),
findsOneWidget,
);
});
});
}
/// Inserts an outline block in the document
Future<void> insertOutlineInDocument(WidgetTester tester) async {
// open the actions menu and insert the outline block
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_selectionMenu_outline.tr(),
);
await tester.pumpAndSettle();
}
@@ -1,10 +1,19 @@
import 'package:integration_test/integration_test.dart';
import 'board_test.dart' as board_test;
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_cell_test.dart' as database_cell_test;
import 'database_field_test.dart' as database_field_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_row_page_test.dart' as database_row_page_test;
import 'database_row_test.dart' as database_row_test;
import 'database_setting_test.dart' as database_setting_test;
import 'database_share_test.dart' as database_share_test;
import 'database_sort_test.dart' as database_sort_test;
import 'database_view_test.dart' as database_view_test;
import 'document/document_test_runner.dart' as document_test_runner;
import 'import_files_test.dart' as import_files_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'switch_folder_test.dart' as switch_folder_test;
import 'empty_document_test.dart' as empty_document_test;
import 'open_ai_smart_menu_test.dart' as smart_menu_test;
import 'document_test.dart' as document_test;
/// The main task runner for all integration tests in AppFlowy.
///
@@ -16,8 +25,25 @@ import 'document_test.dart' as document_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
switch_folder_test.main();
board_test.main();
empty_document_test.main();
smart_menu_test.main();
document_test.main();
share_markdown_test.main();
import_files_test.main();
// Document integration tests
document_test_runner.startTesting();
// Database integration tests
database_cell_test.main();
database_field_test.main();
database_share_test.main();
database_row_page_test.main();
database_row_test.main();
database_setting_test.main();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
// board_test.main();
// empty_document_test.main();
// smart_menu_test.main();
}
@@ -0,0 +1,99 @@
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('share markdown in document page', () {
testWidgets('click the share button in document page', (tester) async {
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
// mock the file picker
final path = await mockSaveFilePath(
p.join(context.applicationDataDirectory, 'test.md'),
);
// click the share button and select markdown
await tester.tapShareButton();
await tester.tapMarkdownButton();
// expect to see the success dialog
tester.expectToExportSuccess();
final file = File(path);
final isExist = file.existsSync();
expect(isExist, true);
final markdown = file.readAsStringSync();
expect(markdown, expectedMarkdown);
});
testWidgets(
'share the markdown after renaming the document name',
(tester) async {
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
// rename the document
await tester.hoverOnPageName(readme);
await tester.renamePage('example');
final shareButton = find.byType(ShareActionList);
final shareButtonState =
tester.state(shareButton) as ShareActionListState;
final path = await mockSaveFilePath(
p.join(
context.applicationDataDirectory,
'${shareButtonState.name}.md',
),
);
// click the share button and select markdown
await tester.tapShareButton();
await tester.tapMarkdownButton();
// expect to see the success dialog
tester.expectToExportSuccess();
final file = File(path);
final isExist = file.existsSync();
expect(isExist, true);
},
);
});
}
const expectedMarkdown = r'''
# Welcome to AppFlowy!
## Here are the basics
- [ ] Click anywhere and just start typing.
- [ ] Highlight any text, and use the editing menu to _style_ **your** <u>writing</u> `however` you ~~like.~~
- [ ] As soon as you type `/` a menu will pop up. Select different types of content blocks you can add.
- [ ] Type `/` followed by `/bullet` or `/num` to create a list.
- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page.
- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
---
## Keyboard shortcuts, markdown, and code block
1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts)
1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown)
1. Type `/code` to insert a code block
## Have a question❓
> Click `?` at the bottom right for help and support.
''';
@@ -1,8 +1,9 @@
import 'package:appflowy/user/presentation/folder/folder_widget.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
@@ -10,84 +11,26 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('customize the folder path', () {
const location = 'appflowy';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(location);
});
tearDownAll(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets(
'customize folder name and path when launching app in first time',
(tester) async {
const folderName = 'appflowy';
await TestFolder.cleanTestLocation(folderName);
await tester.initializeAppFlowy();
// Click create button
await tester.tapCreateButton();
// Set directory
final cfw = find.byType(CreateFolderWidget);
expect(cfw, findsOneWidget);
final state = tester.state(cfw) as CreateFolderWidgetState;
final dir = await TestFolder.testLocation(null);
state.directory = dir.path;
// input folder name
final ftf = find.byType(FlowyTextField);
expect(ftf, findsOneWidget);
await tester.enterText(ftf, 'appflowy');
// Click create button again
await tester.tapCreateButton();
tester.expectToSeeWelcomePage();
await TestFolder.cleanTestLocation(folderName);
});
testWidgets('open a new folder when launching app in first time',
(tester) async {
const folderName = 'appflowy';
await TestFolder.cleanTestLocation(folderName);
await TestFolder.setTestLocation(folderName);
await tester.initializeAppFlowy();
// tap open button
await mockGetDirectoryPath(folderName);
await tester.tapOpenFolderButton();
await tester.wait(1000);
tester.expectToSeeWelcomePage();
await TestFolder.cleanTestLocation(folderName);
});
testWidgets('switch to B from A, then switch to A again', (tester) async {
const String userA = 'userA';
const String userB = 'userB';
const userA = 'UserA';
const userB = 'UserB';
await TestFolder.cleanTestLocation(userA);
await TestFolder.setTestLocation(userA);
await tester.initializeAppFlowy();
final initialPath = p.join(userA, appFlowyDataFolder);
final context = await tester.initializeAppFlowy(
pathExtension: initialPath,
);
// remove the last extension
final rootPath = context.applicationDataDirectory.replaceFirst(
initialPath,
'',
);
await tester.tapGoButton();
tester.expectToSeeWelcomePage();
tester.expectToSeeHomePage();
// switch to user B
{
// set user name for userA
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userA);
@@ -96,28 +39,33 @@ void main() {
await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(userB);
await mockGetDirectoryPath(
p.join(rootPath, userB),
);
await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
tester.expectToSeeWelcomePage();
tester.expectToSeeHomePage();
// set user name for userB
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userB);
}
// switch to the userA
{
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userB);
await tester.openSettingsPage(SettingsPage.files);
await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(userA);
await mockGetDirectoryPath(
p.join(rootPath, userA),
);
await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
tester.expectToSeeWelcomePage();
expect(find.textContaining(userA), findsOneWidget);
tester.expectToSeeHomePage();
tester.expectToSeeUserName(userA);
}
// switch to the userB again
@@ -127,16 +75,15 @@ void main() {
await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(userB);
await mockGetDirectoryPath(
p.join(rootPath, userB),
);
await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
tester.expectToSeeWelcomePage();
expect(find.textContaining(userB), findsOneWidget);
tester.expectToSeeHomePage();
tester.expectToSeeUserName(userB);
}
await TestFolder.cleanTestLocation(userA);
await TestFolder.cleanTestLocation(userB);
});
testWidgets('reset to default location', (tester) async {
@@ -145,7 +92,7 @@ void main() {
await tester.tapGoButton();
// home and readme document
tester.expectToSeeWelcomePage();
tester.expectToSeeHomePage();
// open settings and restore the location
await tester.openSettings();
@@ -153,8 +100,8 @@ void main() {
await tester.restoreLocation();
expect(
await TestFolder.defaultDevelopmentLocation(),
await TestFolder.currentLocation(),
await appFlowyApplicationDataDirectory().then((value) => value.path),
await getIt<ApplicationDataStorage>().getPath(),
);
});
});
@@ -1,88 +1,95 @@
import 'dart:io';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/main.dart' as app;
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as p;
class TestFolder {
/// Location / Path
class FlowyTestContext {
FlowyTestContext({
required this.applicationDataDirectory,
});
/// Set a given AppFlowy data storage location under test environment.
///
/// To pass null means clear the location.
///
/// The file_picker is a system component and can't be tapped, so using logic instead of tapping.
///
static Future<void> setTestLocation(String? name) async {
final location = await testLocation(name);
SharedPreferences.setMockInitialValues({
KVKeys.pathLocation: location.path,
});
return;
}
/// Clean the location.
static Future<void> cleanTestLocation(String? name) async {
final dir = await testLocation(name);
await dir.delete(recursive: true);
return;
}
/// Get current using location.
static Future<String> currentLocation() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(KVKeys.pathLocation)!;
}
/// Get default location under development environment.
static Future<String> defaultDevelopmentLocation() async {
final dir = await appFlowyDocumentDirectory();
return dir.path;
}
/// Get default location under test environment.
static Future<Directory> testLocation(String? name) async {
final dir = await getApplicationDocumentsDirectory();
var path = '${dir.path}/flowy_test';
if (name != null) {
path += '/$name';
}
return Directory(path).create(recursive: true);
}
final String applicationDataDirectory;
}
extension AppFlowyTestBase on WidgetTester {
Future<void> initializeAppFlowy() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('hotkey_manager'),
(MethodCall methodCall) async {
if (methodCall.method == 'unregisterAll') {
// do nothing
}
Future<FlowyTestContext> initializeAppFlowy({
// use to append after the application data directory
String? pathExtension,
Size windowsSize = const Size(1600, 1200),
}) async {
binding.setSurfaceSize(windowsSize);
return;
},
mockHotKeyManagerHandlers();
final directory = await mockApplicationDataStorage(
pathExtension: pathExtension,
);
WidgetsFlutterBinding.ensureInitialized();
await FlowyRunner.run(
FlowyApp(),
IntegrationMode.integrationTest,
);
await app.main();
await wait(3000);
await pumpAndSettle(const Duration(seconds: 2));
return FlowyTestContext(
applicationDataDirectory: directory,
);
}
void mockHotKeyManagerHandlers() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
(MethodCall methodCall) async {
if (methodCall.method == 'unregisterAll') {
// do nothing
}
return;
});
}
Future<String> mockApplicationDataStorage({
// use to append after the application data directory
String? pathExtension,
}) async {
final dir = await getTemporaryDirectory();
// Use a random uuid to avoid conflict.
String path = p.join(dir.path, 'appflowy_integration_test', uuid());
if (pathExtension != null && pathExtension.isNotEmpty) {
path = '$path/$pathExtension';
}
final directory = Directory(path);
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
MockApplicationDataStorage.initialPath = directory.path;
return directory.path;
}
Future<void> tapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = true,
bool warnIfMissed = false,
int milliseconds = 500,
}) async {
await tap(finder);
await tap(
finder,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
}
@@ -91,7 +98,16 @@ extension AppFlowyTestBase on WidgetTester {
String tr, {
int milliseconds = 500,
}) async {
final button = find.text(tr, findRichText: true);
Finder button = find.text(
tr,
findRichText: true,
skipOffstage: false,
);
if (button.evaluate().isEmpty) {
button = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == tr,
);
}
await tapButton(
button,
milliseconds: milliseconds,
@@ -111,8 +127,53 @@ extension AppFlowyTestBase on WidgetTester {
return;
}
Future<void> doubleTapAt(
Offset location, {
int? pointer,
int buttons = kPrimaryButton,
int milliseconds = 500,
}) async {
await tapAt(location, pointer: pointer, buttons: buttons);
await pump(kDoubleTapMinTime);
await tapAt(location, pointer: pointer, buttons: buttons);
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> doubleTapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = true,
int milliseconds = 500,
}) async {
await tap(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pump(kDoubleTapMinTime);
await tap(
finder,
buttons: buttons,
pointer: pointer,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> wait(int milliseconds) async {
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
}
}
extension AppFlowyFinderTestBase on CommonFinders {
Finder findTextInFlowyText(String text) {
return find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == text,
);
}
}
@@ -0,0 +1,309 @@
import 'dart:ui';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
extension CommonOperations on WidgetTester {
/// Tap the GetStart button on the launch page.
Future<void> tapGoButton() async {
final goButton = find.byType(GoButton);
await tapButton(goButton);
}
/// Tap the + button on the home page.
Future<void> tapAddButton() async {
final addButton = find.byType(AddButton);
await tapButton(addButton);
}
/// Tap the create document button.
///
/// Must call [tapAddButton] first.
Future<void> tapCreateDocumentButton() async {
await tapButtonWithName(LocaleKeys.document_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddButton] first.
Future<void> tapCreateGridButton() async {
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddButton] first.
Future<void> tapCreateCalendarButton() async {
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
}
/// Tap the import button.
///
/// Must call [tapAddButton] first.
Future<void> tapImportButton() async {
await tapButtonWithName(LocaleKeys.moreAction_import.tr());
}
/// Tap the import from text & markdown button.
///
/// Must call [tapImportButton] first.
Future<void> tapTextAndMarkdownButton() async {
await tapButtonWithName(LocaleKeys.importPanel_textAndMarkdown.tr());
}
/// Tap the LanguageSelectorOnWelcomePage widget on the launch page.
Future<void> tapLanguageSelectorOnWelcomePage() async {
final languageSelector = find.byType(LanguageSelectorOnWelcomePage);
await tapButton(languageSelector);
}
/// Tap languageItem on LanguageItemsListView.
///
/// [scrollDelta] is the distance to scroll the ListView.
/// Default value is 100
///
/// If it is positive -> scroll down.
///
/// If it is negative -> scroll up.
Future<void> tapLanguageItem({
required String languageCode,
String? countryCode,
double? scrollDelta,
}) async {
final languageItemsListView = find.descendant(
of: find.byType(ListView),
matching: find.byType(Scrollable),
);
final languageItem = find.byWidgetPredicate(
(widget) =>
widget is LanguageItem &&
widget.locale.languageCode == languageCode &&
widget.locale.countryCode == countryCode,
);
// scroll the ListView until zHCNLanguageItem shows on the screen.
await scrollUntilVisible(
languageItem,
scrollDelta ?? 100,
scrollable: languageItemsListView,
// maxHeight of LanguageItemsListView
maxScrolls: 400,
);
try {
await tapButton(languageItem);
} on FlutterError catch (e) {
Log.warn('tapLanguageItem error: $e');
}
}
/// Hover on the widget.
Future<void> hoverOnWidget(
Finder finder, {
Offset? offset,
Future<void> Function()? onHover,
}) async {
try {
final gesture = await createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
await pump();
await gesture.moveTo(offset ?? getCenter(finder));
await pumpAndSettle();
await onHover?.call();
await gesture.removePointer();
} catch (err) {
Log.error('hoverOnWidget error: $err');
}
}
/// Hover on the page name.
Future<void> hoverOnPageName(
String name, {
Future<void> Function()? onHover,
bool useLast = true,
}) async {
if (useLast) {
await hoverOnWidget(findPageName(name).last, onHover: onHover);
} else {
await hoverOnWidget(findPageName(name).first, onHover: onHover);
}
}
/// open the page with given name.
Future<void> openPage(String name) async {
final finder = findPageName(name);
expect(finder, findsOneWidget);
await tapButton(finder);
}
/// Tap the ... button beside the page name.
///
/// Must call [hoverOnPageName] first.
Future<void> tapPageOptionButton() async {
final optionButton = find.byType(ViewDisclosureButton);
await tapButton(optionButton);
}
/// Tap the delete page button.
Future<void> tapDeletePageButton() async {
await tapPageOptionButton();
await tapButtonWithName(ViewDisclosureAction.delete.name);
}
/// Tap the rename page button.
Future<void> tapRenamePageButton() async {
await tapPageOptionButton();
await tapButtonWithName(ViewDisclosureAction.rename.name);
}
/// Rename the page.
Future<void> renamePage(String name) async {
await tapRenamePageButton();
await enterText(find.byType(TextFormField), name);
await tapOKButton();
}
Future<void> tapOKButton() async {
final okButton = find.byWidgetPredicate(
(widget) =>
widget is PrimaryTextButton &&
widget.label == LocaleKeys.button_OK.tr(),
);
await tapButton(okButton);
}
/// Tap the restore button.
///
/// the restore button will show after the current page is deleted.
Future<void> tapRestoreButton() async {
final restoreButton = find.textContaining(
LocaleKeys.deletePagePrompt_restore.tr(),
);
await tapButton(restoreButton);
}
/// Tap the delete permanently button.
///
/// the restore button will show after the current page is deleted.
Future<void> tapDeletePermanentlyButton() async {
final restoreButton = find.textContaining(
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
);
await tapButton(restoreButton);
}
/// Tap the share button above the document page.
Future<void> tapShareButton() async {
final shareButton = find.byWidgetPredicate(
(widget) => widget is DocumentShareButton,
);
await tapButton(shareButton);
}
/// Tap the export markdown button
///
/// Must call [tapShareButton] first.
Future<void> tapMarkdownButton() async {
final markdownButton = find.textContaining(
LocaleKeys.shareAction_markdown.tr(),
);
await tapButton(markdownButton);
}
Future<void> createNewPageWithName(ViewLayoutPB layout, String name) async {
// create a new page
await tapAddButton();
await tapButtonWithName(layout.menuName);
await pumpAndSettle();
// hover on it and change it's name
await hoverOnPageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
onHover: () async {
await renamePage(name);
await pumpAndSettle();
},
);
await pumpAndSettle();
}
Future<void> simulateKeyEvent(
LogicalKeyboardKey key, {
bool isControlPressed = false,
bool isShiftPressed = false,
bool isAltPressed = false,
bool isMetaPressed = false,
}) async {
if (isControlPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.control);
}
if (isShiftPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.shift);
}
if (isAltPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.alt);
}
if (isMetaPressed) {
await simulateKeyDownEvent(LogicalKeyboardKey.meta);
}
await simulateKeyDownEvent(key);
await simulateKeyUpEvent(key);
if (isControlPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.control);
}
if (isShiftPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
}
if (isAltPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.alt);
}
if (isMetaPressed) {
await simulateKeyUpEvent(LogicalKeyboardKey.meta);
}
await pumpAndSettle();
}
}
extension ViewLayoutPBTest on ViewLayoutPB {
String get menuName {
switch (this) {
case ViewLayoutPB.Grid:
return LocaleKeys.grid_menuName.tr();
case ViewLayoutPB.Board:
return LocaleKeys.board_menuName.tr();
case ViewLayoutPB.Document:
return LocaleKeys.document_menuName.tr();
case ViewLayoutPB.Calendar:
return LocaleKeys.calendar_menuName.tr();
default:
throw UnsupportedError('Unsupported layout: $this');
}
}
String get referencedMenuName {
switch (this) {
case ViewLayoutPB.Grid:
return LocaleKeys.document_plugins_referencedGrid.tr();
case ViewLayoutPB.Board:
return LocaleKeys.document_plugins_referencedBoard.tr();
case ViewLayoutPB.Calendar:
return LocaleKeys.document_plugins_referencedCalendar.tr();
default:
throw UnsupportedError('Unsupported layout: $this');
}
}
}
@@ -49,9 +49,11 @@ class TestWorkspaceService {
/// Instructs the application to read workspace data from the workspace found under this [TestWorkspace]'s path.
Future<void> setUpAll() async {
final root = await workspace.root;
final path = root.path;
SharedPreferences.setMockInitialValues(
{
KVKeys.pathLocation: await workspace.root.then((value) => value.path),
KVKeys.pathLocation: path,
},
);
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,178 @@
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
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 'ime.dart';
import 'util.dart';
extension EditorWidgetTester on WidgetTester {
EditorOperations get editor => EditorOperations(this);
}
class EditorOperations {
const EditorOperations(this.tester);
final WidgetTester tester;
EditorState getCurrentEditorState() {
return tester
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
.editorState;
}
/// Tap the line of editor at [index]
Future<void> tapLineOfEditorAt(int index) async {
final textBlocks = find.byType(AppFlowyRichText);
index = index.clamp(0, textBlocks.evaluate().length - 1);
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
await tester.pumpAndSettle();
}
/// Hover on cover plugin button above the document
Future<void> hoverOnCoverToolbar() async {
final coverToolbar = find.byType(DocumentHeaderToolbar);
await tester.startGesture(
tester.getBottomLeft(coverToolbar).translate(5, -5),
kind: PointerDeviceKind.mouse,
);
await tester.pumpAndSettle();
}
/// Taps on the 'Add Icon' button in the cover toolbar
Future<void> tapAddIconButton() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(find.byType(EmojiPopover), findsOneWidget);
}
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
Future<void> tapRemoveIconButton({bool isInPicker = false}) async {
Finder button =
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
if (isInPicker) {
button = find.descendant(of: find.byType(EmojiPopover), matching: button);
}
await tester.tapButton(button);
}
/// Requires that the document must already have an icon. This opens the icon
/// picker
Future<void> tapOnIconWidget() async {
final iconWidget = find.byType(EmojiIconWidget);
await tester.tapButton(iconWidget);
}
Future<void> tapOnAddCover() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
}
Future<void> tapOnChangeCover() async {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
}
Future<void> switchSolidColorBackground() async {
final findPurpleButton = find.byWidgetPredicate(
(widget) => widget is ColorItem && widget.option.colorHex == "ffe8e0ff",
);
await tester.tapButton(findPurpleButton);
}
Future<void> addNetworkImageCover(String imageUrl) async {
final findNewImageButton = find.byType(NewCustomCoverButton);
await tester.tapButton(findNewImageButton);
final imageUrlTextField = find.descendant(
of: find.byType(NetworkImageUrlInput),
matching: find.byType(TextField),
);
await tester.enterText(imageUrlTextField, imageUrl);
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_add.tr(),
);
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
);
}
Future<void> switchNetworkImageCover(String imageUrl) async {
final image = find.byWidgetPredicate(
(widget) => widget is ImageGridItem,
);
await tester.tapButton(image);
}
Future<void> tapOnRemoveCover() async {
await tester.tapButton(find.byType(DeleteCoverButton));
}
/// A cover must be present in the document to function properly since this
/// catches all cover types collectively
Future<void> hoverOnCover() async {
final cover = find.byType(DocumentCover);
await tester.startGesture(
tester.getCenter(cover),
kind: PointerDeviceKind.mouse,
);
await tester.pumpAndSettle();
}
Future<void> dismissCoverPicker() async {
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
}
/// trigger the slash command (selection menu)
Future<void> showSlashMenu() async {
await tester.ime.insertCharacter('/');
}
/// trigger the slash command (selection menu)
Future<void> showAtMenu() async {
await tester.ime.insertCharacter('@');
}
/// Tap the slash menu item with [name]
///
/// Must call [showSlashMenu] first.
Future<void> tapSlashMenuItemWithName(String name) async {
final slashMenuItem = find.text(name, findRichText: true);
await tester.tapButton(slashMenuItem);
}
/// Tap the at menu item with [name]
///
/// Must call [showAtMenu] first.
Future<void> tapAtMenuItemWithName(String name) async {
final atMenuItem = find.descendant(
of: find.byType(SelectionMenuWidget),
matching: find.text(name, findRichText: true),
);
await tester.tapButton(atMenuItem);
}
/// Update the editor's selection
Future<void> updateSelection(Selection selection) async {
final editorState = getCurrentEditorState();
editorState.updateSelectionWithReason(
selection,
reason: SelectionUpdateReason.uiEvent,
);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
extension EmojiTestExtension on WidgetTester {
/// Must call [openEmojiPicker] first
Future<void> switchToEmojiList() async {
final icon = find.byIcon(Icons.tag_faces);
await tapButton(icon);
}
Future<void> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji);
await tapButton(emojiWidget);
}
}
@@ -0,0 +1,135 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
const String readme = 'Read me';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
void expectToSeeHomePage() {
expect(find.byType(HomeStack), findsOneWidget);
expect(find.textContaining(readme), findsWidgets);
}
/// Expect to see the page name on the home page.
void expectToSeePageName(String name) {
final pageName = findPageName(name);
expect(pageName, findsOneWidget);
}
/// Expect not to see the page name on the home page.
void expectNotToSeePageName(String name) {
final pageName = findPageName(name);
expect(pageName, findsNothing);
}
/// Expect to see the document banner.
void expectToSeeDocumentBanner() {
expect(find.byType(DocumentBanner), findsOneWidget);
}
/// Expect not to see the document banner.
void expectNotToSeeDocumentBanner() {
expect(find.byType(DocumentBanner), findsNothing);
}
/// Expect to the markdown file export success dialog.
void expectToExportSuccess() {
final exportSuccess = find.byWidgetPredicate(
(widget) =>
widget is FlowyText &&
widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(),
);
expect(exportSuccess, findsOneWidget);
}
/// Expect to see the add button and icon button in the cover toolbar
void expectToSeePluginAddCoverAndIconButton() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
final addIcon = find.textContaining(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(addCover, findsOneWidget);
expect(addIcon, findsOneWidget);
}
/// Expect to see the document header toolbar empty
void expectToSeeEmptyDocumentHeaderToolbar() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
final addIcon = find.textContaining(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(addCover, findsNothing);
expect(addIcon, findsNothing);
}
void expectToSeeDocumentIcon(String? emoji) {
if (emoji == null) {
final iconWidget = find.byType(EmojiIconWidget);
expect(iconWidget, findsNothing);
return;
}
final iconWidget = find.byWidgetPredicate(
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
);
expect(iconWidget, findsOneWidget);
}
void expectToSeeDocumentCover(CoverType type) {
final findCover = find.byWidgetPredicate(
(widget) => widget is DocumentCover && widget.coverType == type,
);
expect(findCover, findsOneWidget);
}
void expectToSeeNoDocumentCover() {
final findCover = find.byType(DocumentCover);
expect(findCover, findsNothing);
}
void expectChangeCoverAndDeleteButton() {
final findChangeCover = find.text(
LocaleKeys.document_plugins_cover_changeCover.tr(),
);
final findRemoveIcon = find.byType(DeleteCoverButton);
expect(findChangeCover, findsOneWidget);
expect(findRemoveIcon, findsOneWidget);
}
/// Expect to see the user name on the home page
void expectToSeeUserName(String name) {
final userName = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == name,
);
expect(userName, findsOneWidget);
}
/// Expect to see a text
void expectToSeeText(String text) {
Finder textWidget = find.textContaining(text, findRichText: true);
if (textWidget.evaluate().isEmpty) {
textWidget = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == text,
);
}
expect(textWidget, findsOneWidget);
}
/// Find the page name on the home page.
Finder findPageName(String name) {
return find.byWidgetPredicate(
(widget) => widget is ViewSectionItem && widget.view.name == name,
skipOffstage: false,
);
}
}
@@ -0,0 +1,48 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
extension IME on WidgetTester {
IMESimulator get ime => IMESimulator(this);
}
class IMESimulator {
IMESimulator(this.tester) {
client = findTextInputClient();
}
final WidgetTester tester;
late final TextInputClient client;
Future<void> insertText(String text) async {
for (final c in text.characters) {
await insertCharacter(c);
}
}
Future<void> insertCharacter(String character) async {
final value = client.currentTextEditingValue;
if (value == null) {
assert(false);
return;
}
final text = value.text
.replaceRange(value.selection.start, value.selection.end, character);
final textEditingValue = TextEditingValue(
text: text,
selection: TextSelection.collapsed(
offset: value.selection.baseOffset + 1,
),
composing: TextRange.empty,
);
client.updateEditingValue(textEditingValue);
await tester.pumpAndSettle();
}
TextInputClient findTextInputClient() {
final finder = find.byType(KeyboardServiceWidget);
final KeyboardServiceWidgetState state = tester.state(finder);
return state.textInputService as TextInputClient;
}
}
@@ -1,100 +0,0 @@
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
const String readme = 'Read me';
extension AppFlowyLaunch on WidgetTester {
Future<void> tapGoButton() async {
final goButton = find.byType(GoButton);
await tapButton(goButton);
}
Future<void> tapCreateButton() async {
await tapButtonWithName(LocaleKeys.settings_files_create.tr());
}
void expectToSeeWelcomePage() {
expect(find.byType(HomeStack), findsOneWidget);
expect(find.textContaining('Read me'), findsNWidgets(2));
}
Future<void> tapAddButton() async {
final addButton = find.byType(AddButton);
await tapButton(addButton);
}
Future<void> tapCreateDocumentButton() async {
await tapButtonWithName(LocaleKeys.document_menuName.tr());
}
Finder findPageName(String name) {
return find.byWidgetPredicate(
(widget) => widget is ViewSectionItem && widget.view.name == name,
);
}
void expectToSeePageName(String name) {
final pageName = findPageName(name);
expect(pageName, findsOneWidget);
}
void expectNotToSeePageName(String name) {
final pageName = findPageName(name);
expect(pageName, findsNothing);
}
Future<void> hoverOnPageName(String name) async {
final pageName = find.byWidgetPredicate(
(widget) => widget is ViewSectionItem && widget.view.name == name,
);
final gesture = await createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await pump();
await gesture.moveTo(getCenter(pageName));
await pumpAndSettle();
}
Future<void> tapPageOptionButton() async {
final optionButton = find.byType(ViewDisclosureButton);
await tapButton(optionButton);
}
Future<void> tapDeletePageButton() async {
await tapPageOptionButton();
await tapButtonWithName(ViewDisclosureAction.delete.name);
}
void expectToSeeDocumentBanner() {
expect(find.byType(DocumentBanner), findsOneWidget);
}
void expectNotToSeeDocumentBanner() {
expect(find.byType(DocumentBanner), findsNothing);
}
Future<void> tapRestoreButton() async {
final restoreButton = find.textContaining(
LocaleKeys.deletePagePrompt_restore.tr(),
);
await tapButton(restoreButton);
}
Future<void> tapDeletePermanentlyButton() async {
final restoreButton = find.textContaining(
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
);
await tapButton(restoreButton);
}
}
@@ -1,29 +1,94 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_impl.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import '../util.dart';
class MockFilePicker extends FilePicker {
class MockFilePicker implements FilePickerService {
MockFilePicker({
required this.mockPath,
this.mockPath = '',
this.mockPaths = const [],
});
final String mockPath;
final List<String> mockPaths;
@override
Future<String?> getDirectoryPath({String? title}) {
return Future.value(mockPath);
}
@override
Future<String?> saveFile({
String? dialogTitle,
String? fileName,
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
bool lockParentWindow = false,
}) {
return Future.value(mockPath);
}
@override
Future<FilePickerResult?> pickFiles({
String? dialogTitle,
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus p1)? onFileLoading,
bool allowCompression = true,
bool allowMultiple = false,
bool withData = false,
bool withReadStream = false,
bool lockParentWindow = false,
}) {
final platformFiles =
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
return Future.value(
FilePickerResult(
platformFiles,
),
);
}
}
Future<void> mockGetDirectoryPath(String? name) async {
final dir = await TestFolder.testLocation(name);
Future<void> mockGetDirectoryPath(
String path,
) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPath: dir.path,
mockPath: path,
),
);
return;
}
Future<String> mockSaveFilePath(
String path,
) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPath: path,
),
);
return path;
}
Future<List<String>> mockPickFilePaths({
required List<String> paths,
}) async {
// late final Directory dir;
// if (customPath != null) {
// dir = Directory(customPath);
// } else {
// dir = await TestFolder.testLocation(applicationDataPath, name);
// }
// final paths = fileNames.map((e) => p.join(dir.path, e)).toList();
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPaths: paths,
),
);
return paths;
}
@@ -1,33 +1,13 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
enum SettingsPage {
appearance,
language,
files,
user,
}
extension on SettingsPage {
String get name {
switch (this) {
case SettingsPage.appearance:
return LocaleKeys.settings_menu_appearance.tr();
case SettingsPage.language:
return LocaleKeys.settings_menu_language.tr();
case SettingsPage.files:
return LocaleKeys.settings_menu_files.tr();
case SettingsPage.user:
return LocaleKeys.settings_menu_user.tr();
}
}
}
extension AppFlowySettings on WidgetTester {
/// Open settings page
Future<void> openSettings() async {
@@ -41,7 +21,9 @@ extension AppFlowySettings on WidgetTester {
/// Open the page that insides the settings page
Future<void> openSettingsPage(SettingsPage page) async {
final button = find.text(page.name, findRichText: true);
final button = find.byWidgetPredicate(
(widget) => widget is SettingsMenuElement && widget.page == page,
);
expect(button, findsOneWidget);
await tapButton(button);
return;
@@ -50,7 +32,7 @@ extension AppFlowySettings on WidgetTester {
/// Restore the AppFlowy data storage location
Future<void> restoreLocation() async {
final button =
find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr());
find.byTooltip(LocaleKeys.settings_files_recoverLocationTooltips.tr());
expect(button, findsOneWidget);
await tapButton(button);
return;
@@ -64,8 +46,9 @@ extension AppFlowySettings on WidgetTester {
}
Future<void> tapCustomLocationButton() async {
final button =
find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr());
final button = find.byTooltip(
LocaleKeys.settings_files_changeLocationTooltips.tr(),
);
expect(button, findsOneWidget);
await tapButton(button);
return;
@@ -1,4 +1,6 @@
export 'base.dart';
export 'launch.dart';
export 'common_operations.dart';
export 'settings.dart';
export 'data.dart';
export 'expectation.dart';
export 'editor_test_operations.dart';
@@ -7,32 +7,24 @@ class Config {
required String anonKey,
required String key,
required String secret,
required String pgUrl,
required String pgUser,
required String pgPassword,
required String pgPort,
}) async {
final postgresConfig = PostgresConfigurationPB.create()
..url = pgUrl
..userName = pgUser
..password = pgPassword
..port = int.parse(pgPort);
await ConfigEventSetSupabaseConfig(
SupabaseConfigPB.create()
..supabaseUrl = url
..key = key
..anonKey = anonKey
..jwtSecret = secret,
..jwtSecret = secret
..postgresConfig = postgresConfig,
).send();
}
static Future<void> setSupabaseCollabPluginConfig({
required String url,
required String key,
required String jwtSecret,
required String collabTable,
}) async {
final payload = CollabPluginConfigPB.create();
final collabTableConfig = CollabTableConfigPB.create()
..tableName = collabTable;
payload.supabaseConfig = SupabaseDBConfigPB.create()
..supabaseUrl = url
..key = key
..jwtSecret = jwtSecret
..collabTableConfig = collabTableConfig;
await ConfigEventSetCollabPluginConfig(payload).send();
}
}
@@ -18,4 +18,9 @@ class KVKeys {
/// The value is a json string with the following format:
/// {'height': 600.0, 'width': 800.0}
static const String windowSize = 'windowSize';
static const String kDocumentAppearanceFontSize =
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily';
}
+39 -3
View File
@@ -3,6 +3,17 @@ import 'package:envied/envied.dart';
part 'env.g.dart';
/// The environment variables are defined in `.env` file that is located in the
/// appflowy_flutter.
/// Run `dart run build_runner build --delete-conflicting-outputs`
/// to generate the keys from the env file.
///
/// If you want to regenerate the keys, you need to run `dart run
/// build_runner clean` before running `dart run build_runner build
/// --delete-conflicting-outputs`.
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
///
@Envied(path: '.env')
abstract class Env {
@EnviedField(
@@ -32,14 +43,39 @@ abstract class Env {
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_COLLAB_TABLE',
varName: 'SUPABASE_DB',
defaultValue: '',
)
static final String supabaseCollabTable = _Env.supabaseCollabTable;
static final String supabaseDb = _Env.supabaseDb;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_DB_USER',
defaultValue: '',
)
static final String supabaseDbUser = _Env.supabaseDbUser;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_DB_PASSWORD',
defaultValue: '',
)
static final String supabaseDbPassword = _Env.supabaseDbPassword;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_DB_PORT',
defaultValue: '5432',
)
static final String supabaseDbPort = _Env.supabaseDbPort;
}
bool get isSupabaseEnable =>
Env.supabaseUrl.isNotEmpty &&
Env.supabaseAnonKey.isNotEmpty &&
Env.supabaseKey.isNotEmpty &&
Env.supabaseJwtSecret.isNotEmpty;
Env.supabaseJwtSecret.isNotEmpty &&
Env.supabaseDb.isNotEmpty &&
Env.supabaseDbUser.isNotEmpty &&
Env.supabaseDbPassword.isNotEmpty &&
Env.supabaseDbPort.isNotEmpty;
+4 -1
View File
@@ -6,5 +6,8 @@ import 'startup/startup.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlowyRunner.run(FlowyApp());
await FlowyRunner.run(
FlowyApp(),
integrationEnv(),
);
}
@@ -43,7 +43,7 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
@override
Widget buildWidget(PluginContext context) => const BlankPage();
Widget buildWidget({PluginContext? context}) => const BlankPage();
@override
List<NavigationItem> get navigationItems => [this];
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@@ -22,39 +23,45 @@ import 'cell_service.dart';
///
// ignore: must_be_immutable
class CellController<T, D> extends Equatable {
final DatabaseCellContext cellContext;
DatabaseCellContext _cellContext;
final CellCache _cellCache;
final CellCacheKey _cacheKey;
final FieldBackendService _fieldBackendSvc;
final SingleFieldListener _fieldListener;
final CellDataLoader<T> _cellDataLoader;
final CellDataPersistence<D> _cellDataPersistence;
CellListener? _cellListener;
RowMetaListener? _rowMetaListener;
SingleFieldListener? _fieldListener;
CellDataNotifier<T?>? _cellDataNotifier;
VoidCallback? _onCellFieldChanged;
VoidCallback? _onRowMetaChanged;
Timer? _loadDataOperation;
Timer? _saveDataOperation;
String get viewId => cellContext.viewId;
String get viewId => _cellContext.viewId;
RowId get rowId => cellContext.rowId;
RowId get rowId => _cellContext.rowId;
String get fieldId => cellContext.fieldInfo.id;
String get fieldId => _cellContext.fieldInfo.id;
FieldInfo get fieldInfo => cellContext.fieldInfo;
FieldInfo get fieldInfo => _cellContext.fieldInfo;
FieldType get fieldType => cellContext.fieldInfo.fieldType;
FieldType get fieldType => _cellContext.fieldInfo.fieldType;
String? get emoji => _cellContext.emoji;
CellController({
required this.cellContext,
required DatabaseCellContext cellContext,
required CellCache cellCache,
required CellDataLoader<T> cellDataLoader,
required CellDataPersistence<D> cellDataPersistence,
}) : _cellCache = cellCache,
}) : _cellContext = cellContext,
_cellCache = cellCache,
_cellDataLoader = cellDataLoader,
_cellDataPersistence = cellDataPersistence,
_rowMetaListener = RowMetaListener(cellContext.rowId),
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
_fieldBackendSvc = FieldBackendService(
viewId: cellContext.viewId,
@@ -84,20 +91,22 @@ class CellController<T, D> extends Equatable {
);
/// 2.Listen on the field event and load the cell data if needed.
_fieldListener.start(
onFieldChanged: (result) {
result.fold(
(fieldPB) {
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
/// For example:
/// ¥12 -> $12
if (_cellDataLoader.reloadOnFieldChanged) {
_loadData();
}
_onCellFieldChanged?.call();
},
(err) => Log.error(err),
);
_fieldListener?.start(
onFieldChanged: (fieldPB) {
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
/// For example:
/// ¥12 -> $12
if (_cellDataLoader.reloadOnFieldChanged) {
_loadData();
}
_onCellFieldChanged?.call();
},
);
_rowMetaListener?.start(
callback: (newRowMeta) {
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
_onRowMetaChanged?.call();
},
);
}
@@ -105,9 +114,11 @@ class CellController<T, D> extends Equatable {
/// Listen on the cell content or field changes
VoidCallback? startListening({
required void Function(T?) onCellChanged,
VoidCallback? onRowMetaChanged,
VoidCallback? onCellFieldChanged,
}) {
_onCellFieldChanged = onCellFieldChanged;
_onRowMetaChanged = onRowMetaChanged;
/// Notify the listener, the cell data was changed.
onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
@@ -186,18 +197,26 @@ class CellController<T, D> extends Equatable {
}
Future<void> dispose() async {
await _rowMetaListener?.stop();
_rowMetaListener = null;
await _cellListener?.stop();
_cellListener = null;
await _fieldListener?.stop();
_fieldListener = null;
_loadDataOperation?.cancel();
_saveDataOperation?.cancel();
_cellDataNotifier?.dispose();
await _fieldListener.stop();
_cellDataNotifier = null;
_onRowMetaChanged = null;
}
@override
List<Object> get props => [
_cellCache.get(_cacheKey) ?? "",
cellContext.rowId + cellContext.fieldInfo.id
_cellContext.rowId + _cellContext.fieldInfo.id
];
}
@@ -47,7 +47,7 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
@override
Future<Option<FlowyError>> save(DateCellData data) {
final payload = DateChangesetPB.create()
..cellPath = _makeCellPath(cellContext);
..cellId = _makeCellPath(cellContext);
if (data.dateTime != null) {
final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString();
payload.date = date;
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'package:dartz/dartz.dart';
@@ -52,18 +53,23 @@ class CellBackendService {
class DatabaseCellContext with _$DatabaseCellContext {
const factory DatabaseCellContext({
required String viewId,
required RowId rowId,
required RowMetaPB rowMeta,
required FieldInfo fieldInfo,
}) = _DatabaseCellContext;
// ignore: unused_element
const DatabaseCellContext._();
String get rowId => rowMeta.id;
String get fieldId => fieldInfo.id;
FieldType get fieldType => fieldInfo.fieldType;
ValueKey key() {
return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}");
}
/// Only the primary field can have an emoji.
String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null;
}
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
@@ -7,17 +6,23 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
class ChecklistCellBackendService {
final DatabaseCellContext cellContext;
final String viewId;
final String fieldId;
final String rowId;
ChecklistCellBackendService({required this.cellContext});
ChecklistCellBackendService({
required this.viewId,
required this.fieldId,
required this.rowId,
});
Future<Either<Unit, FlowyError>> create({
required String name,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellContext.viewId
..fieldId = cellContext.fieldInfo.id
..rowId = cellContext.rowId
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..insertOptions.add(name);
return DatabaseEventUpdateChecklistCell(payload).send();
@@ -27,9 +32,9 @@ class ChecklistCellBackendService {
required List<String> optionIds,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellContext.viewId
..fieldId = cellContext.fieldInfo.id
..rowId = cellContext.rowId
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..deleteOptionIds.addAll(optionIds);
return DatabaseEventUpdateChecklistCell(payload).send();
@@ -39,9 +44,9 @@ class ChecklistCellBackendService {
required String optionId,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellContext.viewId
..fieldId = cellContext.fieldInfo.id
..rowId = cellContext.rowId
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..selectedOptionIds.add(optionId);
return DatabaseEventUpdateChecklistCell(payload).send();
@@ -51,9 +56,9 @@ class ChecklistCellBackendService {
required SelectOptionPB option,
}) {
final payload = ChecklistCellDataChangesetPB.create()
..viewId = cellContext.viewId
..fieldId = cellContext.fieldInfo.id
..rowId = cellContext.rowId
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..updateOptions.add(option);
return DatabaseEventUpdateChecklistCell(payload).send();
@@ -61,10 +66,9 @@ class ChecklistCellBackendService {
Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
final payload = CellIdPB.create()
..fieldId = cellContext.fieldInfo.id
..viewId = cellContext.viewId
..rowId = cellContext.rowId
..rowId = cellContext.rowId;
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
return DatabaseEventGetChecklistCellData(payload).send();
}
@@ -1,6 +1,4 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -8,12 +6,15 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
class SelectOptionCellBackendService {
final DatabaseCellContext cellContext;
SelectOptionCellBackendService({required this.cellContext});
final String viewId;
final String fieldId;
final String rowId;
String get viewId => cellContext.viewId;
String get fieldId => cellContext.fieldInfo.id;
RowId get rowId => cellContext.rowId;
SelectOptionCellBackendService({
required this.viewId,
required this.fieldId,
required this.rowId,
});
Future<Either<Unit, FlowyError>> create({
required String name,
@@ -1,8 +1,8 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart';
@@ -15,6 +15,7 @@ import 'dart:async';
import 'package:dartz/dartz.dart';
import 'database_view_service.dart';
import 'defines.dart';
import 'layout/layout_service.dart';
import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart';
import 'group/group_listener.dart';
@@ -49,16 +50,11 @@ class DatabaseLayoutSettingCallbacks {
});
}
class CalendarLayoutCallbacks {
final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged;
CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
}
class DatabaseCallbacks {
OnDatabaseChanged? onDatabaseChanged;
OnFieldsChanged? onFieldsChanged;
OnFiltersChanged? onFiltersChanged;
OnSortsChanged? onSortsChanged;
OnNumOfRowsChanged? onNumOfRowsChanged;
OnRowsDeleted? onRowsDeleted;
OnRowsUpdated? onRowsUpdated;
@@ -69,6 +65,7 @@ class DatabaseCallbacks {
this.onNumOfRowsChanged,
this.onFieldsChanged,
this.onFiltersChanged,
this.onSortsChanged,
this.onRowsUpdated,
this.onRowsDeleted,
this.onRowsCreated,
@@ -79,30 +76,29 @@ class DatabaseController {
final String viewId;
final DatabaseViewBackendService _databaseViewBackendSvc;
final FieldController fieldController;
DatabaseLayoutPB? databaseLayout;
DatabaseLayoutPB databaseLayout;
DatabaseLayoutSettingPB? databaseLayoutSetting;
late DatabaseViewCache _viewCache;
// Callbacks
DatabaseCallbacks? _databaseCallbacks;
GroupCallbacks? _groupCallbacks;
DatabaseLayoutSettingCallbacks? _layoutCallbacks;
CalendarLayoutCallbacks? _calendarLayoutCallbacks;
final List<DatabaseCallbacks> _databaseCallbacks = [];
final List<GroupCallbacks> _groupCallbacks = [];
final List<DatabaseLayoutSettingCallbacks> _layoutCallbacks = [];
// Getters
RowCache get rowCache => _viewCache.rowCache;
// Listener
final DatabaseGroupListener groupListener;
final DatabaseLayoutSettingListener layoutListener;
final DatabaseCalendarLayoutListener calendarLayoutListener;
final DatabaseGroupListener _groupListener;
final DatabaseLayoutSettingListener _layoutListener;
DatabaseController({required ViewPB view})
: viewId = view.id,
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
fieldController = FieldController(viewId: view.id),
groupListener = DatabaseGroupListener(view.id),
layoutListener = DatabaseLayoutSettingListener(view.id),
calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
_groupListener = DatabaseGroupListener(view.id),
databaseLayout = databaseLayoutFromViewLayout(view.layout),
_layoutListener = DatabaseLayoutSettingListener(view.id) {
_viewCache = DatabaseViewCache(
viewId: viewId,
fieldController: fieldController,
@@ -113,44 +109,51 @@ class DatabaseController {
_listenOnLayoutChanged();
}
void setListener({
void addListener({
DatabaseCallbacks? onDatabaseChanged,
DatabaseLayoutSettingCallbacks? onLayoutChanged,
GroupCallbacks? onGroupChanged,
CalendarLayoutCallbacks? onCalendarLayoutChanged,
}) {
_layoutCallbacks = onLayoutChanged;
_databaseCallbacks = onDatabaseChanged;
_groupCallbacks = onGroupChanged;
_calendarLayoutCallbacks = onCalendarLayoutChanged;
if (onLayoutChanged != null) {
_layoutCallbacks.add(onLayoutChanged);
}
if (onDatabaseChanged != null) {
_databaseCallbacks.add(onDatabaseChanged);
}
if (onGroupChanged != null) {
_groupCallbacks.add(onGroupChanged);
}
}
Future<Either<Unit, FlowyError>> open() async {
return _databaseViewBackendSvc.openGrid().then((result) {
return _databaseViewBackendSvc.openDatabase().then((result) {
return result.fold(
(database) async {
(DatabasePB database) async {
databaseLayout = database.layoutType;
if (databaseLayout == DatabaseLayoutPB.Calendar) {
_listenOnCalendarLayoutChanged();
}
_databaseCallbacks?.onDatabaseChanged?.call(database);
_viewCache.rowCache.setInitialRows(database.rows);
return await fieldController
.loadFields(
// Load the actual database field data.
final fieldsOrFail = await fieldController.loadFields(
fieldIds: database.fields,
)
.then(
(result) {
return result.fold(
(l) => Future(() async {
await _loadGroups();
await _loadLayoutSetting();
return left(l);
}),
(err) => right(err),
);
);
return fieldsOrFail.fold(
(fields) {
// Notify the database is changed after the fields are loaded.
// The database won't can't be used until the fields are loaded.
for (final callback in _databaseCallbacks) {
callback.onDatabaseChanged?.call(database);
}
_viewCache.rowCache.setInitialRows(database.rows);
return Future(() async {
await _loadGroups();
await _loadLayoutSetting();
return left(fields);
});
},
(err) {
Log.error(err);
return right(err);
},
);
},
@@ -159,7 +162,7 @@ class DatabaseController {
});
}
Future<Either<RowPB, FlowyError>> createRow({
Future<Either<RowMetaPB, FlowyError>> createRow({
RowId? startRowId,
String? groupId,
void Function(RowDataBuilder builder)? withCells,
@@ -180,9 +183,9 @@ class DatabaseController {
}
Future<Either<Unit, FlowyError>> moveGroupRow({
required RowPB fromRow,
required RowMetaPB fromRow,
required String groupId,
RowPB? toRow,
RowMetaPB? toRow,
}) {
return _databaseViewBackendSvc.moveGroupRow(
fromRowId: fromRow.id,
@@ -192,12 +195,12 @@ class DatabaseController {
}
Future<Either<Unit, FlowyError>> moveRow({
required RowPB fromRow,
required RowPB toRow,
required String fromRowId,
required String toRowId,
}) {
return _databaseViewBackendSvc.moveRow(
fromRowId: fromRow.id,
toRowId: toRow.id,
fromRowId: fromRowId,
toRowId: toRowId,
);
}
@@ -211,11 +214,14 @@ class DatabaseController {
);
}
Future<void> updateCalenderLayoutSetting(
CalendarLayoutSettingPB layoutSetting,
Future<void> updateLayoutSetting(
CalendarLayoutSettingPB calendarlLayoutSetting,
) async {
await _databaseViewBackendSvc
.updateLayoutSetting(calendarLayoutSetting: layoutSetting)
.updateLayoutSetting(
calendarLayoutSetting: calendarlLayoutSetting,
layoutType: databaseLayout,
)
.then((result) {
result.fold((l) => null, (r) => Log.error(r));
});
@@ -224,12 +230,11 @@ class DatabaseController {
Future<void> dispose() async {
await _databaseViewBackendSvc.closeView();
await fieldController.dispose();
await groupListener.stop();
await _groupListener.stop();
await _viewCache.dispose();
_databaseCallbacks = null;
_groupCallbacks = null;
_layoutCallbacks = null;
_calendarLayoutCallbacks = null;
_databaseCallbacks.clear();
_groupCallbacks.clear();
_layoutCallbacks.clear();
}
Future<void> _loadGroups() async {
@@ -237,7 +242,9 @@ class DatabaseController {
return Future(
() => result.fold(
(groups) {
_groupCallbacks?.onGroupByField?.call(groups.items);
for (final callback in _groupCallbacks) {
callback.onGroupByField?.call(groups.items);
}
},
(err) => Log.error(err),
),
@@ -245,60 +252,88 @@ class DatabaseController {
}
Future<void> _loadLayoutSetting() async {
if (databaseLayout != null) {
_databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) {
result.fold(
(l) => _layoutCallbacks?.onLoadLayout(l),
(r) => Log.error(r),
);
});
}
_databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) {
result.fold(
(newDatabaseLayoutSetting) {
databaseLayoutSetting = newDatabaseLayoutSetting;
databaseLayoutSetting?.freeze();
for (final callback in _layoutCallbacks) {
callback.onLoadLayout(newDatabaseLayoutSetting);
}
},
(r) => Log.error(r),
);
});
}
void _listenOnRowsChanged() {
final callbacks = DatabaseViewCallbacks(
onNumOfRowsChanged: (rows, rowByRowId, reason) {
_databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
for (final callback in _databaseCallbacks) {
callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
}
},
onRowsDeleted: (ids) {
_databaseCallbacks?.onRowsDeleted?.call(ids);
for (final callback in _databaseCallbacks) {
callback.onRowsDeleted?.call(ids);
}
},
onRowsUpdated: (ids) {
_databaseCallbacks?.onRowsUpdated?.call(ids);
onRowsUpdated: (ids, reason) {
for (final callback in _databaseCallbacks) {
callback.onRowsUpdated?.call(ids, reason);
}
},
onRowsCreated: (ids) {
_databaseCallbacks?.onRowsCreated?.call(ids);
for (final callback in _databaseCallbacks) {
callback.onRowsCreated?.call(ids);
}
},
);
_viewCache.setListener(callbacks);
_viewCache.addListener(callbacks);
}
void _listenOnFieldsChanged() {
fieldController.addListener(
onReceiveFields: (fields) {
_databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields));
for (final callback in _databaseCallbacks) {
callback.onFieldsChanged?.call(UnmodifiableListView(fields));
}
},
onSorts: (sorts) {
for (final callback in _databaseCallbacks) {
callback.onSortsChanged?.call(sorts);
}
},
onFilters: (filters) {
_databaseCallbacks?.onFiltersChanged?.call(filters);
for (final callback in _databaseCallbacks) {
callback.onFiltersChanged?.call(filters);
}
},
);
}
void _listenOnGroupChanged() {
groupListener.start(
_groupListener.start(
onNumOfGroupsChanged: (result) {
result.fold(
(changeset) {
if (changeset.updateGroups.isNotEmpty) {
_groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups);
for (final callback in _groupCallbacks) {
callback.onUpdateGroup?.call(changeset.updateGroups);
}
}
if (changeset.deletedGroups.isNotEmpty) {
_groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups);
for (final callback in _groupCallbacks) {
callback.onDeleteGroup?.call(changeset.deletedGroups);
}
}
for (final insertedGroup in changeset.insertedGroups) {
_groupCallbacks?.onInsertGroup?.call(insertedGroup);
for (final callback in _groupCallbacks) {
callback.onInsertGroup?.call(insertedGroup);
}
}
},
(r) => Log.error(r),
@@ -307,7 +342,9 @@ class DatabaseController {
onGroupByNewField: (result) {
result.fold(
(groups) {
_groupCallbacks?.onGroupByField?.call(groups);
for (final callback in _groupCallbacks) {
callback.onGroupByField?.call(groups);
}
},
(r) => Log.error(r),
);
@@ -316,24 +353,16 @@ class DatabaseController {
}
void _listenOnLayoutChanged() {
layoutListener.start(
_layoutListener.start(
onLayoutChanged: (result) {
result.fold(
(l) {
_layoutCallbacks?.onLayoutChanged(l);
},
(r) => Log.error(r),
);
},
);
}
(newLayout) {
databaseLayoutSetting = newLayout;
databaseLayoutSetting?.freeze();
void _listenOnCalendarLayoutChanged() {
calendarLayoutListener.start(
onCalendarLayoutChanged: (result) {
result.fold(
(l) {
_calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
for (final callback in _layoutCallbacks) {
callback.onLayoutChanged(newLayout);
}
},
(r) => Log.error(r),
);
@@ -25,12 +25,12 @@ class DatabaseViewBackendService {
.then((value) => value.leftMap((l) => l.value));
}
Future<Either<DatabasePB, FlowyError>> openGrid() async {
Future<Either<DatabasePB, FlowyError>> openDatabase() async {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();
}
Future<Either<RowPB, FlowyError>> createRow({
Future<Either<RowMetaPB, FlowyError>> createRow({
RowId? startRowId,
String? groupId,
Map<String, String>? cellDataByFieldId,
@@ -113,9 +113,12 @@ class DatabaseViewBackendService {
}
Future<Either<Unit, FlowyError>> updateLayoutSetting({
required DatabaseLayoutPB layoutType,
CalendarLayoutSettingPB? calendarLayoutSetting,
}) {
final payload = LayoutSettingChangesetPB.create()..viewId = viewId;
final payload = LayoutSettingChangesetPB.create()
..viewId = viewId
..layoutType = layoutType;
if (calendarLayoutSetting != null) {
payload.calendar = calendarLayoutSetting;
}
@@ -1,5 +1,6 @@
import 'dart:collection';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -10,10 +11,14 @@ import 'row/row_service.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnSortsChanged = void Function(List<SortInfo>);
typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<RowId> ids);
typedef OnRowsUpdated = void Function(List<RowId> ids);
typedef OnRowsUpdated = void Function(
List<RowId> ids,
RowsChangedReason reason,
);
typedef OnRowsDeleted = void Function(List<RowId> ids);
typedef OnNumOfRowsChanged = void Function(
UnmodifiableListView<RowInfo> rows,
@@ -1,4 +1,3 @@
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -52,14 +51,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
void _startListening() {
_fieldListener.start(
onFieldChanged: (result) {
onFieldChanged: (updatedField) {
if (isClosed) {
return;
}
result.fold(
(field) => add(FieldCellEvent.didReceiveFieldUpdate(field)),
(err) => Log.error(err),
);
add(FieldCellEvent.didReceiveFieldUpdate(updatedField));
},
);
}
@@ -33,7 +33,8 @@ class _GridFieldNotifier extends ChangeNotifier {
notifyListeners();
}
List<FieldInfo> get fieldInfos => _fieldInfos;
UnmodifiableListView<FieldInfo> get fieldInfos =>
UnmodifiableListView(_fieldInfos);
}
class _GridFilterNotifier extends ChangeNotifier {
@@ -85,9 +86,11 @@ class FieldController {
final FilterBackendService _filterBackendSvc;
final SortBackendService _sortBackendSvc;
bool _isDisposed = false;
// Field callbacks
final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
final _GridFieldNotifier _fieldNotifier = _GridFieldNotifier();
// Field updated callbacks
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
@@ -107,15 +110,15 @@ class FieldController {
final Map<String, SortPB> _sortPBByFieldId = {};
// Getters
List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];
List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
FieldInfo? getField(String fieldId) {
final fields = _fieldNotifier?.fieldInfos
.where((element) => element.id == fieldId)
.toList() ??
[];
final fields = _fieldNotifier.fieldInfos
.where((element) => element.id == fieldId)
.toList();
if (fields.isEmpty) {
return null;
}
@@ -169,6 +172,10 @@ class FieldController {
_listenOnSortChanged();
_settingBackendSvc.getSetting().then((result) {
if (_isDisposed) {
return;
}
result.fold(
(setting) => _updateSetting(setting),
(err) => Log.error(err),
@@ -257,6 +264,10 @@ class FieldController {
_filtersListener.start(
onFilterChanged: (result) {
if (_isDisposed) {
return;
}
result.fold(
(FilterChangesetNotificationPB changeset) {
final List<FilterInfo> filters = filterInfos;
@@ -351,6 +362,9 @@ class FieldController {
_sortsListener.start(
onSortChanged: (result) {
if (_isDisposed) {
return;
}
result.fold(
(SortChangesetNotificationPB changeset) {
final List<SortInfo> newSortInfos = sortInfos;
@@ -371,6 +385,10 @@ class FieldController {
//Listen on setting changes
_settingListener.start(
onSettingUpdated: (result) {
if (_isDisposed) {
return;
}
result.fold(
(setting) => _updateSetting(setting),
(r) => Log.error(r),
@@ -385,6 +403,9 @@ class FieldController {
onFieldsChanged: (result) {
result.fold(
(changeset) {
if (_isDisposed) {
return;
}
_deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields);
@@ -417,27 +438,29 @@ class FieldController {
}
void _updateFieldInfos() {
if (_fieldNotifier != null) {
for (final field in _fieldNotifier!.fieldInfos) {
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
field._hasFilter = _filterPBByFieldId[field.id] != null;
field._hasSort = _sortPBByFieldId[field.id] != null;
}
_fieldNotifier?.notify();
for (final field in _fieldNotifier.fieldInfos) {
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
field._hasFilter = _filterPBByFieldId[field.id] != null;
field._hasSort = _sortPBByFieldId[field.id] != null;
}
_fieldNotifier.notify();
}
Future<void> dispose() async {
if (_isDisposed) {
Log.warn('FieldController is already disposed');
return;
}
_isDisposed = true;
await _fieldListener.stop();
await _filtersListener.stop();
await _settingListener.stop();
await _sortsListener.stop();
for (final callback in _fieldCallbacks.values) {
_fieldNotifier?.removeListener(callback);
_fieldNotifier.removeListener(callback);
}
_fieldNotifier?.dispose();
_fieldNotifier = null;
_fieldNotifier.dispose();
for (final callback in _filterCallbacks.values) {
_filterNotifier?.removeListener(callback);
@@ -460,7 +483,11 @@ class FieldController {
return Future(
() => result.fold(
(newFields) {
_fieldNotifier?.fieldInfos =
if (_isDisposed) {
return left(unit);
}
_fieldNotifier.fieldInfos =
newFields.map((field) => FieldInfo(field: field)).toList();
_loadFilters();
_loadSorts();
@@ -551,7 +578,7 @@ class FieldController {
}
_fieldCallbacks[onReceiveFields] = callback;
_fieldNotifier?.addListener(callback);
_fieldNotifier.addListener(callback);
}
if (onFilters != null) {
@@ -588,7 +615,7 @@ class FieldController {
if (onFieldsListener != null) {
final callback = _fieldCallbacks.remove(onFieldsListener);
if (callback != null) {
_fieldNotifier?.removeListener(callback);
_fieldNotifier.removeListener(callback);
}
}
if (onFiltersListener != null) {
@@ -616,7 +643,7 @@ class FieldController {
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fieldInfos = newFields;
_fieldNotifier.fieldInfos = newFields;
}
void _insertFields(List<IndexFieldPB> insertedFields) {
@@ -632,7 +659,7 @@ class FieldController {
newFieldInfos.add(fieldInfo);
}
}
_fieldNotifier?.fieldInfos = newFieldInfos;
_fieldNotifier.fieldInfos = newFieldInfos;
}
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
@@ -654,7 +681,7 @@ class FieldController {
}
if (updatedFields.isNotEmpty) {
_fieldNotifier?.fieldInfos = newFields;
_fieldNotifier.fieldInfos = newFields;
}
return updatedFields;
}
@@ -1,4 +1,5 @@
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/log.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
@@ -7,12 +8,11 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
typedef UpdateFieldNotifiedValue = Either<FieldPB, FlowyError>;
typedef UpdateFieldNotifiedValue = FieldPB;
class SingleFieldListener {
final String fieldId;
PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier =
PublishNotifier();
void Function(UpdateFieldNotifiedValue)? _updateFieldNotifier;
DatabaseNotificationListener? _listener;
SingleFieldListener({required this.fieldId});
@@ -20,7 +20,7 @@ class SingleFieldListener {
void start({
required void Function(UpdateFieldNotifiedValue) onFieldChanged,
}) {
_updateFieldNotifier?.addPublishListener(onFieldChanged);
_updateFieldNotifier = onFieldChanged;
_listener = DatabaseNotificationListener(
objectId: fieldId,
handler: _handler,
@@ -34,9 +34,8 @@ class SingleFieldListener {
switch (ty) {
case DatabaseNotification.DidUpdateField:
result.fold(
(payload) =>
_updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
(error) => _updateFieldNotifier?.value = right(error),
(payload) => _updateFieldNotifier?.call(FieldPB.fromBuffer(payload)),
(error) => Log.error(error),
);
break;
default:
@@ -46,7 +45,6 @@ class SingleFieldListener {
Future<void> stop() async {
await _listener?.stop();
_updateFieldNotifier?.dispose();
_updateFieldNotifier = null;
}
}
@@ -99,6 +99,14 @@ class FieldBackendService {
);
});
}
/// Returns the primary field of the view.
static Future<Either<FieldPB, FlowyError>> getPrimaryField({
required String viewId,
}) {
final payload = DatabaseViewIdPB.create()..value = viewId;
return DatabaseEventGetPrimaryField(payload).send();
}
}
@freezed
@@ -1,5 +1,4 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
@@ -10,12 +9,10 @@ class GroupBackendService {
Future<Either<Unit, FlowyError>> groupByField({
required String fieldId,
required FieldType fieldType,
}) {
final payload = GroupByFieldPayloadPB.create()
..viewId = viewId
..fieldId = fieldId
..fieldType = fieldType;
..fieldId = fieldId;
return DatabaseEventSetGroupByField(payload).send();
}
@@ -15,13 +15,13 @@ class DatabaseLayoutBackendService {
}) {
final payload = UpdateViewPayloadPB.create()
..viewId = viewId
..layout = _viewLayoutFromDatabaseLayout(layout);
..layout = viewLayoutFromDatabaseLayout(layout);
return FolderEventUpdateView(payload).send();
}
}
ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
switch (databaseLayout) {
case DatabaseLayoutPB.Board:
return ViewLayoutPB.Board;
@@ -33,3 +33,16 @@ ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
throw UnimplementedError;
}
}
DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) {
switch (viewLayout) {
case ViewLayoutPB.Board:
return DatabaseLayoutPB.Board;
case ViewLayoutPB.Calendar:
return DatabaseLayoutPB.Calendar;
case ViewLayoutPB.Grid:
return DatabaseLayoutPB.Grid;
default:
throw UnimplementedError;
}
}
@@ -0,0 +1,163 @@
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'row_meta_listener.dart';
part 'row_banner_bloc.freezed.dart';
class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
final String viewId;
final RowBackendService _rowBackendSvc;
final RowMetaListener _metaListener;
SingleFieldListener? _fieldListener;
RowBannerBloc({
required this.viewId,
required RowMetaPB rowMeta,
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
_metaListener = RowMetaListener(rowMeta.id),
super(RowBannerState.initial(rowMeta)) {
on<RowBannerEvent>(
(event, emit) async {
event.when(
initial: () async {
_loadPrimaryField();
await _listenRowMeteChanged();
},
didReceiveRowMeta: (RowMetaPB rowMeta) {
emit(
state.copyWith(
rowMeta: rowMeta,
),
);
},
setCover: (String coverURL) {
_updateMeta(coverURL: coverURL);
},
setIcon: (String iconURL) {
_updateMeta(iconURL: iconURL);
},
didReceiveFieldUpdate: (updatedField) {
emit(
state.copyWith(
primaryField: updatedField,
loadingState: const LoadingState.finish(),
),
);
},
);
},
);
}
@override
Future<void> close() async {
await _metaListener.stop();
await _fieldListener?.stop();
_fieldListener = null;
return super.close();
}
Future<void> _loadPrimaryField() async {
final fieldOrError =
await FieldBackendService.getPrimaryField(viewId: viewId);
fieldOrError.fold(
(primaryField) {
if (!isClosed) {
_fieldListener = SingleFieldListener(fieldId: primaryField.id);
_fieldListener?.start(
onFieldChanged: (updatedField) {
if (!isClosed) {
add(RowBannerEvent.didReceiveFieldUpdate(updatedField));
}
},
);
add(RowBannerEvent.didReceiveFieldUpdate(primaryField));
}
},
(r) => Log.error(r),
);
}
/// Listen the changes of the row meta and then update the banner
Future<void> _listenRowMeteChanged() async {
_metaListener.start(
callback: (rowMeta) {
add(RowBannerEvent.didReceiveRowMeta(rowMeta));
},
);
}
/// Update the meta of the row and the view
Future<void> _updateMeta({
String? iconURL,
String? coverURL,
}) async {
// Most of the time, the result is success, so we don't need to handle it.
await _rowBackendSvc
.updateMeta(
iconURL: iconURL,
coverURL: coverURL,
rowId: state.rowMeta.id,
)
.then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
// Set the icon and cover of the view
ViewBackendService.updateView(
viewId: viewId,
iconURL: iconURL,
coverURL: coverURL,
).then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
}
}
@freezed
class RowBannerEvent with _$RowBannerEvent {
const factory RowBannerEvent.initial() = _Initial;
const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
_DidReceiveRowMeta;
const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUdate;
const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
}
@freezed
class RowBannerState with _$RowBannerState {
const factory RowBannerState({
ViewPB? view,
FieldPB? primaryField,
required RowMetaPB rowMeta,
required LoadingState loadingState,
}) = _RowBannerState;
factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState(
rowMeta: rowMetaPB,
loadingState: const LoadingState.loading(),
);
}
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish() = _Finish;
}
Loaded 100 of 849 files, more files were not shown because too many files have changed in this diff. Show more