Compare commits

..
110 Commits
Author SHA1 Message Date
Lucas.Xu 09f5317768 chore: disable team workspace (#5099) 2024-04-09 09:46:53 +08:00
hasanbeder b2f0c4f109 chore: update tr-TR.json (#5095)
This commit updates the Turkish translations for the AppFlowy application. 

Several improvements and fixes have been made to enhance the accuracy and consistency of the translations. Additionally, translations for new features and UI elements have been added.

This update aims to provide a better user experience for Turkish-speaking users of AppFlowy. 🇹🇷

chore(localization): Update Turkish translations
feat(localization): Update Turkish translations (tr-TR.json)
fix(localization): Improve Turkish translations for accuracy and consistency
docs(localization): Add Turkish translations for new features and UI elements
2024-04-09 09:00:46 +08:00
Lucas.Xu da5b369aec feat: get view ancestors (#5096) 2024-04-09 08:58:53 +08:00
Lucas.Xu a92f3d30c2 chore: update editor version & update CHANGELOG (#5094)
* chore: update editor version

* chore: update changelog
2024-04-08 21:40:09 +08:00
Nathan.fooo 5c96a2c5cb chore: fix open workspace (#5092)
* chore: fix open workspace

* chore: fix open workspace
2024-04-08 20:55:02 +08:00
hongttandLucas.Xu 31798959c7 chore: update Vietnamese translations (#5080)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-04-08 20:43:38 +08:00
Kilu.He 5479d3ec6e chore: web build size optimization (#5071)
* chore: deploy test web

fix: update nginx.conf

fix: support https

* fix: support wasm gzip

* chore: optimize web build size

* fix: code review
2024-04-08 20:09:15 +08:00
Lucas.Xu 5777830730 feat: doc auto refresh (#5088)
* feat: doc auto refresh

* chore: update editor version
2024-04-08 18:01:20 +08:00
Nathan.fooo 79ea485a27 chore: update client api (#5087) 2024-04-08 16:13:37 +08:00
Lucas.Xu f0d8eee8a0 fix: doc state refresh (#5086) 2024-04-08 14:06:05 +08:00
Kilu.He 83b18c4825 fix: tauri ci error (#5085) 2024-04-08 12:11:58 +08:00
Lucas.Xu 429892aefc fix: delta insert assert (#5083) 2024-04-08 10:00:00 +08:00
JulesDmn d56de308e1 chore: update French translations (#5077) 2024-04-08 09:34:32 +08:00
Juliano Penna 3171712380 chore: enable more editor text to be translated (#4426) 2024-04-08 09:33:45 +08:00
47d321b8c2 fix: twelve hour parsing issue (#4721)
* fix: twelve hour time format fixed

* fix: time format for 12hour

* feat: auto capitalize am-pm in formatter

* fix: time field placeholder based on user time format

* chore: bugfix + improvements

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2024-04-08 09:33:18 +08:00
Lucas.Xu 9536cde789 feat: add a loading indicator when creating, deleting, or switching workspaces. (#5067) 2024-04-07 23:06:33 +08:00
Nathan.fooo 3e32fac876 chore: pass platform info to rust side (#5079)
* chore: pass platform info to rust side

* chore: pass platform info to rust side

* chore: fix test

* chore: fix test

* chore: fix test

* chore: enable ios log
2024-04-07 21:36:55 +08:00
Nathan.fooo 81594fa796 chore: bump client api (#5078) 2024-04-07 15:22:37 +08:00
Nathan.fooo db6767f61e chore: fix create workspace bugs (#5074) 2024-04-06 21:59:37 +08:00
nathan 5ceb132b0b chore: disable supabase 2024-04-05 13:58:40 +08:00
Nathan.fooo 587de161f6 chore: bump collab and client api (#5069)
* chore: bump collab that do not emit awareness by default

* chore: bump collab
2024-04-05 13:57:34 +08:00
Richard Shiue 7ca3f215a3 chore: rename android ci yaml (#5065) 2024-04-05 09:25:58 +08:00
Sore 2f386ff24d chore: update ckb-KU.json (#5066)
* Update ckb-KU.json

* Update settings.json

* Update project.inlang.json
2024-04-05 09:25:12 +08:00
Nathan.fooo 33dd21f4a5 chore: bump collab that do not emit awareness by default (#5063) 2024-04-04 22:46:54 +08:00
Nathan.fooo 65e7e2347a chore: versionize migration (#5060) 2024-04-04 13:03:40 +08:00
Nathan.fooo 8ade3b5f73 chore: bump client api (#5057) 2024-04-04 10:56:10 +08:00
Lucas.Xu 0ace832374 chore: enable collaborator (#5051) 2024-04-04 09:09:53 +08:00
Lucas.Xu 382ce827c1 fix: workspace integration test (#5049) 2024-04-03 22:30:01 +08:00
Kilu.He c97ece5e81 chore: web and tauri project (#4996)
* chore: web and tauri project

fix: clippy

* fix: update version
2024-04-03 19:25:54 +08:00
Nathan.fooo 046e0bac36 chore: fix awareness test (#5050)
* chore: fix awarenss test

* ci: try fix docker ci

* ci: clippy
2024-04-03 17:16:32 +08:00
Nathan.fooo 58fb529eaa chore: fix user awareness fetch (#5048)
* chore: fix user awareness fetch

* chore: update

* chore: update
2024-04-03 15:43:19 +08:00
Lucas.Xu 38fe61ff69 chore: bump version 0.5.4 (#5047) 2024-04-03 13:29:11 +08:00
Lucas.Xu e8931c5bd2 fix: workspaces empty error in local mode (#5045)
* fix: workspaces empty error in local mode

* fix: not allowed the user delete the only workspace
2024-04-03 12:21:02 +08:00
Nathan.fooo a25c728866 chore: bump collab (#5042)
* chore: bump collab

* chore: bump client api

* chore: fix test
2024-04-03 11:26:48 +08:00
Lucas.Xu e022ee88da fix: disable collaborators in local mode (#5043) 2024-04-03 10:30:22 +08:00
Nathan.fooo dff3ea60bd chore: start init sync when open document/database (#5040) 2024-04-02 22:15:42 +08:00
Nathan.fooo 419600ca11 chore: fix flutter log (#5038) 2024-04-02 20:02:45 +08:00
Kilu.He 4da624d2d3 chore: stop run web ci (#5037) 2024-04-02 15:55:15 +08:00
Kilu.He acec664879 fix: the error of miss update yrs repo (#5035) 2024-04-02 15:14:45 +08:00
Lucas.Xu 682f2a6a3b chore: replace close_document with close_view (#5033) 2024-04-02 13:36:59 +08:00
nathan 5a00361207 chore: remove doc state check that download from remote 2024-04-02 12:57:38 +08:00
Lucas.Xu 096a01ed44 fix: remove the deleted workspace from local storage (#5026)
* fix: remove the deleted workspace from local storage

* fix: unable to get latest workspaces on mobile

* fix: unable to get latest workspaces on desktop

* chore: try to fix ios ci

* fix: user workspace menu flash

* Revert "chore: try to fix ios ci"

This reverts commit 4a1e8bcb9d.
2024-04-02 11:28:05 +08:00
Nathan.fooo 1816b15b55 feat: fetch missing updates (#5030)
* chore: using patch yrs

* chore: bump client api

* chore: bump client api

* chore: bump client api

* chore: bump client api

* chore: bump collab

* chore: bump client api

* chore: bump client api

* chore: bump client api
2024-04-02 10:14:09 +08:00
Lucas.Xu 7576796e2f feat: filter recent views not belong to current user (#5014)
* feat: filter recent views not belong to current user

* fix: filter the trash ids and other private view ids
2024-04-02 09:57:14 +08:00
Lucas.Xu 555649c535 chore: upgrade android min sdk version to 23(android 6.0) (#5029) 2024-04-01 22:27:09 +08:00
Richard Shiue 440253f0d9 chore: restrict successful url launch to only http/https (#5028) 2024-04-01 21:09:38 +08:00
Lucas.Xu 75e51fdeea fix: favorite section doesn't update after switching workspace (#5025) 2024-04-01 15:34:30 +08:00
Lucas.Xu 723423d423 feat: support moving view across sections (#5015) 2024-04-01 14:27:29 +08:00
Lucas.Xu 893d23d6a3 fix: duplicated selections from remote (#5023)
* fix: duplicated selections from remote

* fix: docker ci
2024-04-01 12:08:52 +08:00
Lucas.Xu 932a403b5d feat: enable collab workspace features (#5012)
* feat: enable collab workspace features

* feat: change collaborator background color
2024-04-01 09:07:38 +08:00
Richard Shiue 53dbef25ab chore: more select option cell editor improvements (#5019) 2024-03-31 19:03:02 +08:00
Simon WordshiftyandRichard Shiue 129d56e494 chore: update sv-SE.json (#4916)
* Fink 🐦: update translations

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2024-03-31 12:15:51 +08:00
TheRetroFAN 3f9f53c7a1 chore: update de-DE translations (#4994) 2024-03-31 12:09:35 +08:00
Richard Shiue 419464c175 feat: select option cell editor revamp (#5011)
* chore: gen new select option color on frontend

* chore: reorder select options

* chore: fix performance regression

* chore: add text field tap region

* chore: implement hover focus

* chore: implement keyboard focus

* chore: fix tests

* chore: reorder options in field editor

* chore: fix tests
2024-03-31 10:54:17 +08:00
Nathan.fooo adc2ee755e chore: remove lru (#5008)
* chore: remove lru

* chore: update logs

* chore: clippy
2024-03-30 16:28:24 +08:00
Nathan.fooo c2c84a5812 chore: Init sync when missing updates (#5001)
* chore: bump client api

* chore: update

* chore: update

* chore: bump client api

* chore: bump client api
2024-03-29 19:34:51 +08:00
Lucas.Xu 0e7b3c7db2 feat: support leaving workspace (#5007) 2024-03-29 19:01:43 +08:00
Lucas.Xu dc8f632e3e feat: database sync indicator (#5005)
* feat: database sync indicator

* fix: sync state error

* fix: ios ci
2024-03-29 17:37:02 +08:00
ZackandLucas.Xu 3f4a409364 feat: leave workspace api (#4942)
* feat: leave workpspace event

* feat: add leave workspace event

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-03-29 16:53:59 +08:00
Richard Shiue 490cb48354 chore: organize code (#5004)
* chore: rename files and classes

* chore: move some files around
2024-03-29 11:54:42 +08:00
Richard Shiue 97575d4f6a fix: single select option filter ui (#5002) 2024-03-29 10:47:04 +08:00
crazeteam e16e8693c8 chore: remove repetitive words (#4992)
Signed-off-by: crazeteam <lilujing@outlook.com>
2024-03-28 17:52:33 +08:00
Lucas.Xu 60acf8c889 feat: collab cursor/selection (#4983)
* feat: support collab selection

* feat: collab cusro/selection

* chore: add metadata field

* feat: support displaying user name above cursor

* fix: emit error

* feat: support displaying collaborators

* feat: sync collaborator

* fix: collab doc issues

* chore: update deps

* feat: refactor device id

* chore: enable share button

* chore: update collab a816214

* fix: clippy lint

* chore: use extension type instead class function

* feat: add clear recent views button in debug mode

* chore: support clear recent views

* feat: support saving the last opened workspace

* chore: update collab
2024-03-28 17:46:31 +08:00
Lucas.Xu bf98a627b9 fix: workspace names align issue (#4993) 2024-03-26 15:36:48 +08:00
Lucas.Xu 6e5b346f25 feat: show member count on mobile (#4991)
* feat: show member count on mobile

* fix: favorite section not sync after switching workspace

* fix: favorite page will throw an error

* fix: flutter analyze
2024-03-26 14:52:48 +08:00
Lucas.Xu a1b183f330 feat: support for switching workspace on mobile (#4990)
* feat: support switching workspaces on mobile

* fix: sync recent section
2024-03-26 11:21:49 +08:00
Mayur Mahajan b8e3de97a5 docs: replace broken links with working ones (#4988)
* docs: replace broken links with working ones

* fix: some more links

- I made a mistake in the first commit for the link of installing from Mac/windows/linux
- replaced the link for roadmap
2024-03-26 10:52:50 +08:00
Nathan.fooo 84559afefa chore: bump collab that fixs database row sync issue (#4985) 2024-03-26 07:34:37 +08:00
Lucas.Xu de3e7ca9be fix: create new page error on mobile (#4984)
* chore: update translations

* fix: remove sidebar_root_view_bloc

* fix: remove sidebar_root_view_bloc

* chore: fix ios ci

* feat: customize image cache path
2024-03-25 23:08:52 +08:00
Nathan.fooo 701384cd74 chore: bump collab rev (#4982)
* chore: bump collab rev

* chore: bump collab rev

* chore: bump client api
2024-03-25 16:15:46 +08:00
Richard Shiue 477e43b5e6 chore: adjust select option editor (#4980)
* chore: adjust wording of hint

* chore: remove unnecessary add button

* chore: pdate select_option_editor.dart

* chore: udate en.json
2024-03-25 11:58:10 +08:00
Lucas.Xu 755083509f fix: discord notify ci (#4978) 2024-03-25 10:19:50 +08:00
Nathan.fooo 91e00d3782 chore: bump client api (#4976) 2024-03-25 08:13:49 +08:00
Nathan.fooo 3f0b3478c1 chore: bump client api that deduplicate sync message (#4972)
* chore: bump client api

* chore: update log

* chore: bump client api
2024-03-24 07:31:41 +08:00
Nathan.fooo b307312a71 chore: fix database row sync (#4964)
* chore: fix database row sync

* ci: fix test

* ci: fix web build

* chore: bump collab
2024-03-23 09:18:47 +08:00
Lucas.Xu c0642d3ff3 fix: collab workspace issues (#4961) 2024-03-22 17:15:18 +08:00
Richard Shiue 99ee60a60d fix: launch review issues (#4960)
* fix: i18n for multi select condition list

* fix: lookup url cell content to assert validity

* fix: compromise checkmark color
2024-03-22 16:50:01 +08:00
Lucas.Xu 27ff5f07ab Revert "feat: folder search mvp (#4665)" (#4962)
This reverts commit c1006c18c3.
2024-03-22 15:15:38 +08:00
Mathias Mogensen e2e38f72bb feat: clear all cells (#4856)
* feat: clear all cells

* fix: smaller dialog width

* fix: clippy warning
2024-03-21 17:40:23 +01:00
c1006c18c3 feat: folder search mvp (#4665)
* feat: implement folder indexer

* feat: sqlite search views using fts5

* feat: add view indexing to user manager

* feat: implement folder indexer

* feat: add sqlite search documents

* feat: add document indexing to user manager

* feat: add document indexing to folder indexer

* chore: update collab rev

* feat: search frontend integration

* refactor: search index

* test: add event test

* chore: fix ci

* feat: initial command palette overlay impl (#4619)

* chore: test search engine

* chore: initial structure

* chore: replace old search request

* chore: enable log for lib-dispatch

* chore: move search manager to core

* feat: move traits and responsibility to search crate

* feat: move search to search crate

* feat: replace sqlite with tantivy

* feat: deserialize tantivy documents

* chore: fixes after rebase

* chore: clean code

* feat: fetch and sort results

* fix: code review + cleaning

* feat: support custom icons

* feat: support view layout icons

* feat: rename bloc and fix indexing

* fix: prettify dialog

* feat: score results

* chore: update collab rev

* feat: add recent view history to command palette

* test: add integration_tests

* fix: clippy changes

* fix: focus traversal in cmd palette

* fix: remove file after merging main

* chore: code review and panic-safe

* feat: index all views if index does not exist

* chore: improve logic with conditional

* chore: add is_empty check

* chore: abstract logic from folder manager init

* chore: update collab rev

* chore: code review

* chore: fixes after merge + update lock file

* chore: revert cargo lock

* fix: set icon type when removing icon

* fix: code review + dependency inversion

* fix: remove icon fix for not persisting icon type

* test: simple tests manipulating views

* test: create 100 views

* fix: tauri build

* chore: create 1000 views

* chore: create util methods

* chore: test

* chore: test

* chore: remove logs

* chore: fix build.rs

* chore: export models

* chore: enable clear cache on Rust-CI

* fix: navigate to newly created views

* fix: force disable setting workspace listener on rebuilds

* fix: remove late final

* fix: missing returns

* fix: localization and minor fixes

* test: add index assert to large test

* fix: missing section param after merging main

* chore: try fix unzip file error

* chore: lower the test

* feat: show hint when result is in trash

---------

Co-authored-by: nathan <nathan@appflowy.io>
Co-authored-by: Jiraffe7 <twajxjiraffe@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-03-21 17:34:53 +01:00
Kilu.He 4e99952b0e feat: support editor default shortcuts (#4943)
fix: pasted bugs
2024-03-21 21:52:48 +08:00
Richard Shiue 98876b149f ci: remove workaround for windows flutter ci (#4957) 2024-03-21 21:31:05 +08:00
Kilu.He 370f8a6558 feat: support custom scrollbar for document (#4936)
feat: support keywords for slash list to search

feat: support right-click to copy,pasted,cut

fix: the hint text should follow the align setting

feat: support get/set latest view

feat: support to show snackbar after delete page

fix: some bugs
2024-03-21 21:12:37 +08:00
Lucas.Xu 40b710d140 chore: enable feature flags in settings (#4956) 2024-03-21 14:20:43 +08:00
Zack 8c8be741b0 feat: support error for limit exceed (#4947) 2024-03-21 13:38:48 +08:00
Mathias MogensenandLucas.Xu 96af517c65 chore: bump version 0.5.3 (#4954)
* chore: bump version 0.5.3

* chore: update CHANGELOG.md

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-03-21 13:27:25 +08:00
Lucas.Xu 5f8ef3856a feat: initial version for collab document (#4937)
* feat: initial version for collab document

* feat: show sync indicator

* feat: add sync document feature flag

* fix: rust ci

* chore: remove unused code

* chore: update doc_bloc.dart
2024-03-21 13:26:48 +08:00
Richard Shiue 37f521ae57 chore: kanban filter mvp (#4935)
* chore: add filter controller in group controller

* chore: enable url filter

* chore: bump collab rev

* chore: fix test

* chore: bump collab rev
2024-03-21 12:54:56 +08:00
Lucas.Xu ef9891abfe feat: support private section (#4882) 2024-03-21 12:02:03 +08:00
Mukund-TandonandLucas.Xu 9201cd6347 fix: import table from markdown (#4881)
* fix: fixed error while importing table from markdown

* test: added test for the changes made

* fix: made changes from code review

* chore: remove the force unwrap code

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-03-20 21:33:39 +08:00
Kilu.He 73df51f35f feat: support list item style (#4934)
* fix: wrong offset of mention panel

feat: support list item style

* feat: support backward selection when the end selection is empty
2024-03-20 18:29:13 +08:00
Richard Shiue b2fb631500 fix: use bg color for mobile url bottom sheet (#4944) 2024-03-20 17:24:09 +08:00
Lucas.Xu 7a9cb03c8e refactor: improve mobile toolbar design (v3) (#4891)
* refactor: mobile toolbar v3 design

* feat: customize color item

* feat: close keyboard directly when clicking the close button even opening Aa menu

* feat: enable undo/redo in editing list mode
2024-03-20 15:41:12 +08:00
Nathan.fooo d19fa5a34a chore: support skip gc (#4926)
* chore: support skip gc

* chore: fix test
2024-03-20 14:34:50 +08:00
Lucas.Xu 822d62b562 feat: support 32-bit android devices (#4940)
* feat: support 32-bit android devices

* chore: update appflowy_editor
2024-03-20 13:08:32 +08:00
Kilu.He cb617cd9d3 feat: support markdown for heading 4-6 and inline math (#4917)
* feat: support-OAuth-login

* fix: optimize editor experience and fix bugs (0315)
2024-03-18 18:42:19 +08:00
Lucas.Xu 7375349626 fix: legacy bugs on mobile (#4927) 2024-03-18 14:06:12 +08:00
Richard Shiue fdc105a3e8 fix: docker ci (#4925) 2024-03-18 08:00:01 +08:00
Richard Shiue b557f89829 refactor: group controller traits (#4880)
* chore: remove async trait

* chore: require From<AnyMap> for trait TypeOption

* refactor: simplify group controller by removing GroupController trait

* chore: rename GroupControllerOperation trait to GroupController

* chore: add some docs

* chore: remove plural struct and use Vec directly

* chore: unnecessary mut

* chore: use same name to indicate passthrough

* chore: remove unused trait

* chore: rename group context to group controller context

* chore: remove group name

* chore: move type option interceptor to GroupCustomize and split off delegates for group context and group controller

* chore: adapt tests to changes

* chore: adapt flutter frontend to changes

* chore: code cleanup

* chore: fix clippy and adapt tauri frontend to changes

* chore: group controller code clean up

* chore: no need to pass cell data when moving row

* chore: rename some functions and variables

* chore: remove content filter
2024-03-16 17:18:40 +08:00
Richard Shiue 0f006fa60b feat: pre-fill row cells with filter data before row creation (#4854)
* feat: fill cells according to active filters

* chore: short circuit filter_row function

* chore: delete corresponding filters when filtered filter is deleted

* chore: validate filters when loading

* chore: remove unnecessary tuple in return

* chore: use trait

* chore: add tests
2024-03-16 17:18:30 +08:00
Lucas.Xu 452974ab99 feat: support el_GR language (#4922) 2024-03-16 11:53:34 +08:00
ひもの a16196eee5 chore: update ja-JP translations (#4921) 2024-03-16 10:29:06 +08:00
Anthony Belshi 31a33fdbfd feat: support el-GR language (#4920) 2024-03-16 09:50:51 +08:00
Nathan.foooandRichard Shiue bb414c3fd6 chore: enable relation to (#4866)
* chore: enable relation to

* chore: fix database name and improve UI

* chore: remove database view id from relation type option

* chore: add remove row id test

* chore: improve appearance of untitled rows

* chore: empty in row detail

* fix: cannot add events after closing

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2024-03-15 22:58:55 +08:00
T I Z E NandLucas.Xu 8d01d54e7f chore: update german translations (#4865)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-03-15 22:58:17 +08:00
Kilu.He 6d4cfe7316 feat: support-OAuth-login (#4899)
* feat: support-OAuth-login

* fix: modified ubuntu version and devtool

* fix: ts lint error
2024-03-15 20:29:00 +08:00
q200892907 57e3a2ce68 fix: apply formatting to pasted text only when the block is a paragraph (#4906) 2024-03-15 14:38:27 +08:00
Lucas.Xu ac34617e51 feat: support changing the workspace icon (#4898) 2024-03-15 14:10:30 +08:00
Abhijeet Singh 8776ac5c36 chore: update Spanish translations (#4896) 2024-03-15 09:35:19 +08:00
Lucas.Xu 3e1b4d81c1 fix: mobile keyboard issues (#4903) 2024-03-15 09:03:38 +08:00
876 changed files with 42133 additions and 9122 deletions

No files matched your search

-7
View File
@@ -37,13 +37,6 @@ runs:
override: true
profile: minimal
- name: Export pub environment variables and add to PATH
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
fi
shell: bash
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
File renamed without changes.
+73
View File
@@ -0,0 +1,73 @@
name: Deploy Web (Test)
on:
push:
branches:
- build/test
- main
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
env:
SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }}
SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }}
SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }}
ENV_FILE: test.env
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
pnpm install
- name: copy env file
working-directory: frontend/appflowy_web_app
run: |
cp ${{ env.ENV_FILE }} .env
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
- name: build
working-directory: frontend/appflowy_web_app
run: |
pnpm run build
- name: generate SSL certificate
run: |
echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt
echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key
- name: Deploy to EC2
uses: easingthemes/ssh-deploy@main
with:
SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }}
ARGS: "-rlgoDzvc -i"
SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key"
REMOTE_HOST: ${{ env.REMOTE_HOST }}
REMOTE_USER: ${{ env.REMOTE_USER }}
EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/"
SCRIPT_AFTER: |
docker build -t appflowy-web-app .
docker rm -f appflowy-web-app || true
docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app
+11 -8
View File
@@ -7,19 +7,13 @@ on:
- release/*
paths:
- frontend/**
pull_request:
branches:
- main
- release/*
paths:
- frontend/**
types:
- opened
- synchronize
- reopened
- unlocked
- ready_for_review
types: [opened, synchronize, reopened, unlocked, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -33,6 +27,15 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Docker Compose
run: |
docker-compose --version || {
echo "Docker Compose not found, installing..."
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
}
- name: Build the app
shell: bash
run: |
@@ -45,4 +48,4 @@ jobs:
else \
echo "$line"; \
fi; \
done \
done
+2
View File
@@ -7,6 +7,7 @@ on:
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
- ".github/actions/flutter_build/**"
- "frontend/rust-lib/**"
- "frontend/appflowy_flutter/**"
- "frontend/resources/**"
@@ -17,6 +18,7 @@ on:
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
- ".github/actions/flutter_build/**"
- "frontend/rust-lib/**"
- "frontend/appflowy_flutter/**"
- "frontend/resources/**"
+4 -3
View File
@@ -86,6 +86,7 @@ jobs:
model: 'iPhone 15'
shutdown_after_job: false
- name: Run integration tests
working-directory: frontend/appflowy_flutter
run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
+4 -9
View File
@@ -54,13 +54,6 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Export pub environment variable on Windows
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
fi
shell: bash
- name: Install flutter
uses: subosito/flutter-action@v2
with:
@@ -336,6 +329,7 @@ jobs:
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage
LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage
LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz
strategy:
fail-fast: false
@@ -412,7 +406,8 @@ jobs:
continue-on-error: true
run: |
sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }}
cp -r ${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
cd ..
cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
- name: Upload Asset
id: upload-release-asset
@@ -422,7 +417,7 @@ jobs:
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }}
asset_name: ${{ env.LINUX_ZIP_NAME }}
asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }}
asset_content_type: application/octet-stream
- name: Upload Debian package
+2 -1
View File
@@ -93,7 +93,8 @@ jobs:
af_cloud_test_base_url: http://localhost
af_cloud_test_ws_url: ws://localhost/ws/v1
af_cloud_test_gotrue_url: http://localhost/gotrue
run: cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture
run: |
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture
- name: rustfmt rust-lib
run: cargo fmt --all -- --check
+113
View File
@@ -0,0 +1,113 @@
name: Tauri2-CI
on:
pull_request:
paths:
- ".github/workflows/tauri2_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.75"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tauri-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
env:
CI: true
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_web_app/src-tauri -> target"
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
working-directory: frontend
run: |
cargo install --force duckscript_cli
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install cargo-make
working-directory: frontend
run: |
cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
mkdir dist
pnpm install
cd src-tauri && cargo build
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint:tauri
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_web_app
args: "--debug"
+5 -4
View File
@@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest]
platform: [ubuntu-20.04]
runs-on: ${{ matrix.platform }}
@@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
@@ -80,7 +80,7 @@ jobs:
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
if: matrix.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
@@ -110,4 +110,5 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_tauri
projectPath: frontend/appflowy_tauri
args: "--debug"
+5 -5
View File
@@ -31,7 +31,7 @@ jobs:
- platform: macos-latest
args: "--target x86_64-apple-darwin"
target: "macos-x86_64"
- platform: ubuntu-latest
- platform: ubuntu-20.04
args: "--target x86_64-unknown-linux-gnu"
target: "linux-x86_64"
@@ -46,7 +46,7 @@ jobs:
ref: ${{ github.event.inputs.branch }}
- name: Maximize build space (ubuntu only)
if: matrix.settings.platform == 'ubuntu-latest'
if: matrix.settings.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
@@ -88,7 +88,7 @@ jobs:
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-latest'
if: matrix.settings.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
@@ -140,14 +140,14 @@ jobs:
- name: Upload Deb package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-latest'
if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.deb
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
- name: Upload AppImage package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-latest'
if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.AppImage
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
+74
View File
@@ -0,0 +1,74 @@
name: Web2-CI
on:
pull_request:
paths:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
web-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
pnpm install
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
- name: build and analyze
working-directory: frontend/appflowy_web_app
run: |
pnpm run analyze >> analyze-size.txt
- name: Upload analyze-size.txt
uses: actions/upload-artifact@v4
with:
name: analyze-size.txt
path: frontend/appflowy_web_app/analyze-size.txt
retention-days: 30
- name: Upload stats.html
uses: actions/upload-artifact@v4
with:
name: stats.html
path: frontend/appflowy_web_app/dist/stats.html
retention-days: 30
+7 -8
View File
@@ -1,13 +1,12 @@
name: WEB-CI
on:
pull_request:
branches:
- "main"
paths:
- ".github/workflows/web_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_web/**"
workflow_dispatch:
inputs:
build:
description: 'Build the web app'
required: true
default: 'true'
env:
CARGO_TERM_COLOR: always
@@ -22,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest]
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:
+2
View File
@@ -40,3 +40,5 @@ frontend/package
frontend/*.deb
**/Cargo.toml.bak
**/.cargo/**
+24
View File
@@ -1,4 +1,28 @@
# Release Notes
## Version 0.5.4 - 04/08/2024
### New Features
- Introduced support for displaying a synchronization indicator within documents and databases to enhance user awareness of data sync status
- Revamped the select option cell editor in database
- Improved translations for Spanish, German, Kurdish, and Vietnamese
- Supported Android 6 and newer versions
### Bug Fixes
- Resolved an issue where twelve-hour time formats were not being parsed correctly in databases
- Fixed a bug affecting the user interface of the single select option filter
- Fixed various minor UI issues
## Version 0.5.3 - 03/21/2024
### New Features
- Added build support for 32-bit Android devices
- Introduced filters for KanBan boards for enhanced organization
- Introduced the new "Relations" column type in Grids
- Expanded language support with the addition of Greek
- Enhanced toolbar design for Mobile devices
- Introduced a command palette feature with initial support for page search
### Bug Fixes
- Rectified the issue of incomplete row data in Grids when adding new rows with active filters
- Enhanced the logic governing the filtering of number and select/multi-select fields for improved accuracy
- Implemented UI refinements on both Desktop and Mobile platforms, enriching the overall user experience of AppFlowy
## Version 0.5.2 - 03/13/2024
### Bug Fixes
- Import csv file.
+16 -18
View File
@@ -31,15 +31,15 @@ You are in charge of your data and customizations.
## User Installation
* [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages)
* [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker)
* [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source)
- [Windows/Mac/Linux](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/mac-windows-linux-packages)
- [Docker](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/installing-with-docker)
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
## Built With
* [Flutter](https://flutter.dev/)
- [Flutter](https://flutter.dev/)
* [Rust](https://www.rust-lang.org/)
- [Rust](https://www.rust-lang.org/)
## Stay Up-to-Date
@@ -51,8 +51,8 @@ Please view the [documentation](https://docs.appflowy.io/docs/documentation/appf
## Roadmap
* [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap)
* [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
@@ -63,19 +63,17 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
## Contributing
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details.
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
## Translations 🌎🗺
[![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
## Join the community to build AppFlowy together
<a href="https://github.com/AppFlowy-IO/AppFlowy/graphs/contributors">
@@ -92,14 +90,14 @@ When a customer's evolving core needs are not satisfied, they either switch to a
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
* To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
* To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
We decided to achieve this mission by upholding the three most fundamental values:
* Data privacy first
* Reliable native experience
* Community-driven extensibility
- Data privacy first
- Reliable native experience
- Community-driven extensibility
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
@@ -111,6 +109,6 @@ Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppF
Special thanks to these amazing projects which help power AppFlowy.IO:
* [flutter-quill](https://github.com/singerdmx/flutter-quill)
* [cargo-make](https://github.com/sagiegurari/cargo-make)
* [contrib.rocks](https://contrib.rocks)
- [flutter-quill](https://github.com/singerdmx/flutter-quill)
- [cargo-make](https://github.com/sagiegurari/cargo-make)
- [contrib.rocks](https://contrib.rocks)
+4 -1
View File
@@ -115,9 +115,12 @@
},
{
"name": "AF-desktop: Debug Rust",
"request": "attach",
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}"
// To launch the application directly, use the following configuration:
// "request": "launch",
// "program": "[YOUR_APPLICATION_PATH]",
},
{
// https://tauri.app/v1/guides/debugging/vs-code
+5 -5
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.5.2"
APPFLOWY_VERSION = "0.5.4"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -50,7 +50,8 @@ APP_ENVIRONMENT = "local"
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
WEB_LIB_PATH= "appflowy_web/wasm-libs/af-wasm"
WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm"
TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend"
# Test default config
TEST_CRATE_TYPE = "cdylib"
TEST_LIB_EXT = "dylib"
@@ -226,9 +227,8 @@ script = ['''
echo FEATURES: ${FLUTTER_DESKTOP_FEATURES}
echo PRODUCT_EXT: ${PRODUCT_EXT}
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
echo ${platforms}
echo ${BUILD_ARCHS}
echo ${BUILD_VERSION}
echo BUILD_ARCHS: ${BUILD_ARCHS}
echo BUILD_VERSION: ${BUILD_VERSION}
''']
script_runner = "@shell"
@@ -52,7 +52,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
minSdkVersion 29
minSdkVersion 23
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -11,6 +11,12 @@ file(COPY
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/arm64-v8a
)
# armeabi-v7a
file(COPY
${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/armeabi-v7a
)
# x86_64
file(COPY
${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/x86_64/libc++_shared.so
@@ -0,0 +1,11 @@
# AppFlowy Test Markdown import with table
# Table
| S.No. | Column 2 |
| --- | --- |
| 1. | row 1 |
| 2. | row 2 |
| 3. | row 3 |
| 4. | row 4 |
| 5. | row 5 |
@@ -1,6 +1,7 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'dart:ui';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -14,8 +15,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
@@ -1,9 +1,12 @@
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'collaborative_workspace_test.dart' as collaboration_workspace_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
// import 'document_sync_test.dart' as document_sync_test;
import 'user_setting_sync_test.dart' as user_sync_test;
import 'workspace/change_name_and_icon_test.dart'
as change_workspace_name_and_icon_test;
import 'workspace/collaborative_workspace_test.dart'
as collaboration_workspace_test;
Future<void> main() async {
preset_af_cloud_env_test.main();
@@ -16,5 +19,7 @@ Future<void> main() async {
anon_user_continue_test.main();
// workspace
collaboration_workspace_test.main();
change_workspace_name_and_icon_test.main();
}
@@ -0,0 +1,81 @@
// ignore_for_file: unused_import
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const icon = '😄';
const name = 'AppFlowy';
final email = '${uuid()}@appflowy.io';
testWidgets('change name and icon', (tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email, // use the same email to check the next test
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
var workspaceIcon = tester.widget<WorkspaceIcon>(
find.byType(WorkspaceIcon),
);
expect(workspaceIcon.workspace.icon, '');
await tester.openWorkspaceMenu();
await tester.changeWorkspaceIcon(icon);
await tester.changeWorkspaceName(name);
workspaceIcon = tester.widget<WorkspaceIcon>(
find.byType(WorkspaceIcon),
);
expect(workspaceIcon.workspace.icon, icon);
expect(find.findTextInFlowyText(name), findsOneWidget);
});
testWidgets('verify the result again after relaunching', (tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email, // use the same email to check the next test
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// check the result again
final workspaceIcon = tester.widget<WorkspaceIcon>(
find.byType(WorkspaceIcon),
);
expect(workspaceIcon.workspace.icon, icon);
expect(workspaceIcon.workspace.name, name);
});
}
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
@@ -23,11 +24,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/database_test_op.dart';
import '../shared/dir.dart';
import '../shared/emoji.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../shared/database_test_op.dart';
import '../../shared/dir.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -35,14 +36,14 @@ void main() {
final email = '${uuid()}@appflowy.io';
group('collaborative workspace', () {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
// combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it',
(tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
@@ -51,32 +52,23 @@ void main() {
await tester.expectToSeeHomePageWithGetStartedPage();
const name = 'AppFlowy.IO';
// the workspace will be opened after created
await tester.createCollaborativeWorkspace(name);
// see the success message
var success = find.text(LocaleKeys.workspace_createSuccess.tr());
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
final loading = find.byType(Loading);
await tester.pumpUntilNotFound(loading);
// check the create result
Finder success;
// delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu();
var items = find.byType(WorkspaceMenuItem);
final Finder items = find.byType(WorkspaceMenuItem);
expect(items, findsNWidgets(2));
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
name,
);
// open the newly created workspace
await tester.tapButton(items.last);
success = find.text(LocaleKeys.workspace_openSuccess.tr());
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
await tester.closeCollaborativeWorkspaceMenu();
// delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu();
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
await tester.hoverOnWidget(
secondWorkspace,
@@ -96,20 +88,11 @@ void main() {
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// delete success
success = find.text(LocaleKeys.workspace_createSuccess.tr());
await tester.pumpUntilFound(success);
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
},
);
// check the result
await tester.openCollaborativeWorkspaceMenu();
items = find.byType(WorkspaceMenuItem);
expect(items, findsOneWidget);
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name != name,
true,
);
await tester.closeCollaborativeWorkspaceMenu();
});
});
}
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/select/select_option.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -327,69 +327,70 @@ void main() {
);
});
testWidgets('last modified and created at field type options',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Disable this test because it fails on CI randomly
// testWidgets('last modified and created at field type options',
// (tester) async {
// await tester.initializeAppFlowy();
// await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
final created = DateTime.now();
// await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// final created = DateTime.now();
// create a created at field
await tester.tapNewPropertyButton();
await tester.renameField(FieldType.CreatedTime.i18n);
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.CreatedTime);
await tester.dismissFieldEditor();
// // create a created at field
// await tester.tapNewPropertyButton();
// await tester.renameField(FieldType.CreatedTime.i18n);
// await tester.tapSwitchFieldTypeButton();
// await tester.selectFieldType(FieldType.CreatedTime);
// await tester.dismissFieldEditor();
// create a last modified field
await tester.tapNewPropertyButton();
await tester.renameField(FieldType.LastEditedTime.i18n);
await tester.tapSwitchFieldTypeButton();
// // create a last modified field
// await tester.tapNewPropertyButton();
// await tester.renameField(FieldType.LastEditedTime.i18n);
// await tester.tapSwitchFieldTypeButton();
// get time just before modifying
final modified = DateTime.now();
// // get time just before modifying
// final modified = DateTime.now();
// create a last modified field (cont'd)
await tester.selectFieldType(FieldType.LastEditedTime);
await tester.dismissFieldEditor();
// // create a last modified field (cont'd)
// await tester.selectFieldType(FieldType.LastEditedTime);
// await tester.dismissFieldEditor();
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.CreatedTime,
content: DateFormat('MMM dd, y HH:mm').format(created),
);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.LastEditedTime,
content: DateFormat('MMM dd, y HH:mm').format(modified),
);
// tester.assertCellContent(
// rowIndex: 0,
// fieldType: FieldType.CreatedTime,
// content: DateFormat('MMM dd, y HH:mm').format(created),
// );
// tester.assertCellContent(
// rowIndex: 0,
// fieldType: FieldType.LastEditedTime,
// content: DateFormat('MMM dd, y HH:mm').format(modified),
// );
// open field editor and change date & time format
await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n);
await tester.tapEditFieldButton();
await tester.changeDateFormat();
await tester.changeTimeFormat();
await tester.dismissFieldEditor();
// // open field editor and change date & time format
// await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n);
// await tester.tapEditFieldButton();
// await tester.changeDateFormat();
// await tester.changeTimeFormat();
// await tester.dismissFieldEditor();
// open field editor and change date & time format
await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n);
await tester.tapEditFieldButton();
await tester.changeDateFormat();
await tester.changeTimeFormat();
await tester.dismissFieldEditor();
// // open field editor and change date & time format
// await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n);
// await tester.tapEditFieldButton();
// await tester.changeDateFormat();
// await tester.changeTimeFormat();
// await tester.dismissFieldEditor();
// assert format has been changed
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.CreatedTime,
content: DateFormat('dd/MM/y hh:mm a').format(created),
);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.LastEditedTime,
content: DateFormat('dd/MM/y hh:mm a').format(modified),
);
});
// // assert format has been changed
// tester.assertCellContent(
// rowIndex: 0,
// fieldType: FieldType.CreatedTime,
// content: DateFormat('dd/MM/y hh:mm a').format(created),
// );
// tester.assertCellContent(
// rowIndex: 0,
// fieldType: FieldType.LastEditedTime,
// content: DateFormat('dd/MM/y hh:mm a').format(modified),
// );
// });
});
}
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -13,10 +13,10 @@ void main() {
group('sidebar expand test', () {
bool isExpanded({required FolderCategoryType type}) {
if (type == FolderCategoryType.personal) {
if (type == FolderCategoryType.private) {
return find
.descendant(
of: find.byType(PersonalFolder),
of: find.byType(PrivateSectionFolder),
matching: find.byType(ViewItem),
)
.evaluate()
@@ -30,19 +30,19 @@ void main() {
await tester.tapGoButton();
// first time is expanded
expect(isExpanded(type: FolderCategoryType.personal), true);
expect(isExpanded(type: FolderCategoryType.private), true);
// collapse the personal folder
await tester.tapButton(
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
);
expect(isExpanded(type: FolderCategoryType.personal), false);
expect(isExpanded(type: FolderCategoryType.private), false);
// expand the personal folder
await tester.tapButton(
find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
);
expect(isExpanded(type: FolderCategoryType.personal), true);
expect(isExpanded(type: FolderCategoryType.private), true);
});
});
}
@@ -1,5 +1,5 @@
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
@@ -1,6 +1,5 @@
import 'package:integration_test/integration_test.dart';
import 'sidebar_expand_test.dart' as sidebar_expanded_test;
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_test.dart' as sidebar_test;
@@ -10,7 +9,7 @@ void startTesting() {
// Sidebar integration tests
sidebar_test.main();
sidebar_expanded_test.main();
// sidebar_expanded_test.main();
sidebar_favorite_test.main();
sidebar_icon_test.main();
}
@@ -1,5 +1,6 @@
import 'dart:io';
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';
@@ -44,5 +45,44 @@ void main() {
tester.expectToSeePageName('test1');
tester.expectToSeePageName('test2');
});
testWidgets('import markdown file with table', (tester) async {
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a getting started page
tester.expectToSeePageName(gettingStarted);
await tester.tapAddViewButton();
await tester.tapImportButton();
const testFileName = 'markdown_with_table.md';
final paths = <String>[];
final str = await rootBundle.loadString(
'assets/test/workspaces/markdowns/$testFileName',
);
final path = p.join(context.applicationDataDirectory, testFileName);
paths.add(path);
File(path).writeAsStringSync(str);
// mock get files
mockPickFilePaths(
paths: paths,
);
await tester.tapTextAndMarkdownButton();
tester.expectToSeePageName('markdown_with_table');
// expect to see all content of markdown file along with table
await tester.openPage('markdown_with_table');
final importedPageEditorState = tester.editor.getCurrentEditorState();
expect(importedPageEditorState.getNodeAtPath([0])!.type,
HeadingBlockKeys.type,);
expect(importedPageEditorState.getNodeAtPath([2])!.type,
HeadingBlockKeys.type,);
expect(importedPageEditorState.getNodeAtPath([4])!.type,
TableBlockKeys.type,);
});
});
}
@@ -5,9 +5,7 @@ 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 '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
@@ -18,80 +16,80 @@ void main() {
return;
}
testWidgets('switch to B from A, then switch to A again', (tester) async {
const userA = 'UserA';
const userB = 'UserB';
// testWidgets('switch to B from A, then switch to A again', (tester) async {
// const userA = 'UserA';
// const userB = 'UserB';
final initialPath = p.join(userA, appFlowyDataFolder);
final context = await tester.initializeAppFlowy(
pathExtension: initialPath,
);
// remove the last extension
final rootPath = context.applicationDataDirectory.replaceFirst(
initialPath,
'',
);
// 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();
await tester.expectToSeeHomePageWithGetStartedPage();
// await tester.tapGoButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
// switch to user B
{
// set user name for userA
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userA);
// // switch to user B
// {
// // set user name for userA
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.user);
// await tester.enterUserName(userA);
await tester.openSettingsPage(SettingsPage.files);
await tester.pumpAndSettle();
// await tester.openSettingsPage(SettingsPage.files);
// await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(
p.join(rootPath, userB),
);
await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
await tester.expectToSeeHomePageWithGetStartedPage();
// // mock the file_picker result
// await mockGetDirectoryPath(
// p.join(rootPath, userB),
// );
// await tester.tapCustomLocationButton();
// await tester.pumpAndSettle();
// await tester.expectToSeeHomePageWithGetStartedPage();
// set user name for userB
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userB);
}
// // set user name for userB
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.user);
// await tester.enterUserName(userB);
// }
// switch to the userA
{
await tester.openSettingsPage(SettingsPage.files);
await tester.pumpAndSettle();
// // switch to the userA
// {
// await tester.openSettingsPage(SettingsPage.files);
// await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(
p.join(rootPath, userA),
);
await tester.tapCustomLocationButton();
// // mock the file_picker result
// await mockGetDirectoryPath(
// p.join(rootPath, userA),
// );
// await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
await tester.expectToSeeHomePageWithGetStartedPage();
tester.expectToSeeUserName(userA);
}
// await tester.pumpAndSettle();
// await tester.expectToSeeHomePageWithGetStartedPage();
// tester.expectToSeeUserName(userA);
// }
// switch to the userB again
{
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.files);
await tester.pumpAndSettle();
// // switch to the userB again
// {
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.files);
// await tester.pumpAndSettle();
// mock the file_picker result
await mockGetDirectoryPath(
p.join(rootPath, userB),
);
await tester.tapCustomLocationButton();
// // mock the file_picker result
// await mockGetDirectoryPath(
// p.join(rootPath, userB),
// );
// await tester.tapCustomLocationButton();
await tester.pumpAndSettle();
await tester.expectToSeeHomePageWithGetStartedPage();
tester.expectToSeeUserName(userB);
}
});
// await tester.pumpAndSettle();
// await tester.expectToSeeHomePageWithGetStartedPage();
// tester.expectToSeeUserName(userB);
// }
// });
testWidgets('reset to default location', (tester) async {
await tester.initializeAppFlowy();
@@ -0,0 +1,49 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/dir.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('create new page', () {
testWidgets('create document', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.local,
);
// click the anonymousSignInButton
final anonymousSignInButton = find.byType(SignInAnonymousButton);
expect(anonymousSignInButton, findsOneWidget);
await tester.tapButton(anonymousSignInButton);
// tap the create page button
final createPageButton = find.byKey(mobileCreateNewPageButtonKey);
await tester.tapButton(createPageButton);
expect(find.byType(MobileDocumentScreen), findsOneWidget);
});
});
}
@@ -28,9 +28,7 @@ void main() {
group('anonymous sign in on mobile', () {
testWidgets('anon user and then sign in', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.local,
);
await tester.initializeAppFlowy();
// click the anonymousSignInButton
final anonymousSignInButton = find.byType(SignInAnonymousButton);
@@ -180,7 +180,11 @@ extension AppFlowyTestBase on WidgetTester {
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
await pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 5),
);
}
Future<void> tapButtonWithName(
@@ -528,7 +528,7 @@ extension CommonOperations on WidgetTester {
final workspace = find.byType(SidebarWorkspace);
expect(workspace, findsOneWidget);
// click it
await tapButton(workspace);
await tapButton(workspace, milliseconds: 2000);
}
Future<void> closeCollaborativeWorkspaceMenu() async {
@@ -560,7 +560,6 @@ extension CommonOperations on WidgetTester {
// input the workspace name
await enterText(find.byType(TextField), name);
await pumpAndSettle();
await tapButtonWithName(LocaleKeys.button_ok.tr());
}
@@ -1,5 +1,7 @@
import 'dart:io';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -29,10 +31,8 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/discl
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';
@@ -54,7 +54,7 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_edi
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
@@ -0,0 +1,58 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
extension AppFlowyWorkspace on WidgetTester {
/// Open workspace menu
Future<void> openWorkspaceMenu() async {
final workspaceWrapper = find.byType(SidebarSwitchWorkspaceButton);
expect(workspaceWrapper, findsOneWidget);
await tapButton(workspaceWrapper);
final workspaceMenu = find.byType(WorkspacesMenu);
expect(workspaceMenu, findsOneWidget);
}
/// Open a workspace
Future<void> openWorkspace(String name) async {
final workspace = find.descendant(
of: find.byType(WorkspaceMenuItem),
matching: find.findTextInFlowyText(name),
);
expect(workspace, findsOneWidget);
await tapButton(workspace);
}
Future<void> changeWorkspaceName(String name) async {
final moreButton = find.descendant(
of: find.byType(WorkspaceMenuItem),
matching: find.byType(WorkspaceMoreActionList),
);
expect(moreButton, findsOneWidget);
await tapButton(moreButton);
await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr()));
final input = find.byType(TextFormField);
expect(input, findsOneWidget);
await enterText(input, name);
await tapButton(find.text(LocaleKeys.button_ok.tr()));
}
Future<void> changeWorkspaceIcon(String icon) async {
final iconButton = find.descendant(
of: find.byType(WorkspaceMenuItem),
matching: find.byType(WorkspaceIcon),
);
expect(iconButton, findsOneWidget);
await tapButton(iconButton);
final iconPicker = find.byType(FlowyIconPicker);
expect(iconPicker, findsOneWidget);
await tapButton(find.findTextInFlowyText(icon));
}
}
@@ -64,4 +64,9 @@ class KVKeys {
/// The value is a json string with the following format:
/// {'feature_flag_1': true, 'feature_flag_2': false}
static const String featureFlag = 'featureFlag';
/// The key for saving the last opened workspace id
///
/// The workspace id is a string.
static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId';
}
@@ -5,6 +5,9 @@ import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value
const String _source = 'Document';
typedef DocumentNotificationCallback = void Function(
DocumentNotification,
FlowyResult<Uint8List, FlowyError>,
@@ -16,7 +19,8 @@ class DocumentNotificationParser
super.id,
required super.callback,
}) : super(
tyParser: (ty) => DocumentNotification.valueOf(ty),
tyParser: (ty, source) =>
source == _source ? DocumentNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
@@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value
const String _source = 'Workspace';
// Folder
typedef FolderNotificationCallback = void Function(
FolderNotification,
@@ -21,7 +24,8 @@ class FolderNotificationParser
super.id,
required super.callback,
}) : super(
tyParser: (ty) => FolderNotification.valueOf(ty),
tyParser: (ty, source) =>
source == _source ? FolderNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
@@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value
const String _source = 'Database';
// DatabasePB
typedef DatabaseNotificationCallback = void Function(
DatabaseNotification,
@@ -21,7 +24,8 @@ class DatabaseNotificationParser
super.id,
required super.callback,
}) : super(
tyParser: (ty) => DatabaseNotification.valueOf(ty),
tyParser: (ty, source) =>
source == _source ? DatabaseNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
@@ -14,7 +14,7 @@ class NotificationParser<T, E extends Object> {
String? id;
void Function(T, FlowyResult<Uint8List, E>) callback;
E Function(Uint8List) errorParser;
T? Function(int) tyParser;
T? Function(int, String) tyParser;
void parse(SubscribeObject subject) {
if (id != null) {
@@ -23,7 +23,7 @@ class NotificationParser<T, E extends Object> {
}
}
final ty = tyParser(subject.ty);
final ty = tyParser(subject.ty, subject.source);
if (ty == null) {
return;
}
@@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// This value should be the same as the USER_OBSERVABLE_SOURCE value
const String _source = 'User';
// User
typedef UserNotificationCallback = void Function(
UserNotification,
@@ -21,7 +24,8 @@ class UserNotificationParser
required String super.id,
required super.callback,
}) : super(
tyParser: (ty) => UserNotification.valueOf(ty),
tyParser: (ty, source) =>
source == _source ? UserNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
+2
View File
@@ -11,6 +11,7 @@ class AppFlowyConfiguration {
required this.custom_app_path,
required this.origin_app_path,
required this.device_id,
required this.platform,
required this.authenticator_type,
required this.supabase_config,
required this.appflowy_cloud_config,
@@ -25,6 +26,7 @@ class AppFlowyConfiguration {
final String custom_app_path;
final String origin_app_path;
final String device_id;
final String platform;
final int authenticator_type;
final SupabaseConfiguration supabase_config;
final AppFlowyCloudConfiguration appflowy_cloud_config;
@@ -26,7 +26,7 @@ extension on ViewPB {
String get routeName {
switch (layout) {
case ViewLayoutPB.Document:
return MobileEditorScreen.routeName;
return MobileDocumentScreen.routeName;
case ViewLayoutPB.Grid:
return MobileGridScreen.routeName;
case ViewLayoutPB.Calendar:
@@ -42,8 +42,8 @@ extension on ViewPB {
switch (layout) {
case ViewLayoutPB.Document:
return {
MobileEditorScreen.viewId: id,
MobileEditorScreen.viewTitle: name,
MobileDocumentScreen.viewId: id,
MobileDocumentScreen.viewTitle: name,
};
case ViewLayoutPB.Grid:
return {
@@ -77,7 +77,7 @@ class AppBarDoneButton extends StatelessWidget {
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap,
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
padding: const EdgeInsets.all(12),
child: FlowyText(
LocaleKeys.button_done.tr(),
color: Theme.of(context).colorScheme.primary,
@@ -93,7 +93,7 @@ class AppBarSaveButton extends StatelessWidget {
super.key,
required this.onTap,
this.enable = true,
this.padding = const EdgeInsets.fromLTRB(12, 12, 8, 12),
this.padding = const EdgeInsets.all(12),
});
final VoidCallback onTap;
@@ -165,7 +165,7 @@ class AppBarMoreButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
padding: const EdgeInsets.all(12),
onTap: () => onTap(context),
child: const FlowySvg(FlowySvgs.three_dots_s),
);
@@ -4,7 +4,10 @@ import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/shared/sync_indicator.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
@@ -70,7 +73,21 @@ class _MobileViewPageState extends State<MobileViewPage> {
} else {
body = state.data!.fold((view) {
viewPB = view;
actions.add(_buildAppBarMoreButton(view));
actions.addAll([
if (FeatureFlag.syncDocument.isOn) ...[
DocumentCollaborators(
width: 60,
height: 44,
fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 8),
view: view,
),
const HSpace(16.0),
DocumentSyncIndicator(view: view),
const HSpace(8.0),
],
_buildAppBarMoreButton(view),
]);
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
@@ -144,6 +161,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
Widget _buildAppBarMoreButton(ViewPB view) {
return AppBarMoreButton(
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
@@ -183,14 +202,12 @@ class _MobileViewPageState extends State<MobileViewPage> {
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
context.dispatchNotification(
const EditorNotification(type: EditorNotificationType.redo),
);
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
context.dispatchNotification(EditorNotification.redo());
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
@@ -0,0 +1,82 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class BottomSheetCloseButton extends StatelessWidget {
const BottomSheetCloseButton({
super.key,
this.onTap,
});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap ?? () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: 18,
height: 18,
child: FlowySvg(
FlowySvgs.m_bottom_sheet_close_m,
),
),
),
);
}
}
class BottomSheetDoneButton extends StatelessWidget {
const BottomSheetDoneButton({
super.key,
this.onDone,
});
final VoidCallback? onDone;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onDone ?? () => Navigator.pop(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0),
child: FlowyText(
LocaleKeys.button_done.tr(),
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
textAlign: TextAlign.right,
),
),
);
}
}
class BottomSheetBackButton extends StatelessWidget {
const BottomSheetBackButton({
super.key,
this.onTap,
});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap ?? () => Navigator.pop(context),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: 18,
height: 18,
child: FlowySvg(
FlowySvgs.m_app_bar_back_s,
),
),
),
);
}
}
@@ -1,6 +1,4 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -26,7 +24,7 @@ class BottomSheetHeader extends StatelessWidget {
left: 0,
child: Align(
alignment: Alignment.centerLeft,
child: AppBarCloseButton(
child: BottomSheetCloseButton(
onTap: onClose,
),
),
@@ -41,19 +39,8 @@ class BottomSheetHeader extends StatelessWidget {
if (onDone != null)
Align(
alignment: Alignment.centerRight,
child: FlowyButton(
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)),
color: Color(0xFF00BCF0),
),
text: FlowyText.medium(
LocaleKeys.button_done.tr(),
color: Colors.white,
fontSize: 16.0,
),
onTap: onDone,
child: BottomSheetDoneButton(
onDone: onDone,
),
),
],
@@ -1,4 +1,4 @@
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart';
@@ -195,12 +195,12 @@ class BottomSheetHeader extends StatelessWidget {
if (showBackButton)
const Align(
alignment: Alignment.centerLeft,
child: AppBarBackButton(),
child: BottomSheetBackButton(),
),
if (showCloseButton)
const Align(
alignment: Alignment.centerLeft,
child: AppBarCloseButton(),
child: BottomSheetCloseButton(),
),
Align(
child: FlowyText(
@@ -212,8 +212,8 @@ class BottomSheetHeader extends StatelessWidget {
if (showDoneButton)
Align(
alignment: Alignment.centerRight,
child: AppBarDoneButton(
onTap: () => Navigator.pop(context),
child: BottomSheetDoneButton(
onDone: () => Navigator.pop(context),
),
),
],
@@ -199,7 +199,7 @@ class MobileHiddenGroup extends StatelessWidget {
children: [
Expanded(
child: Text(
group.groupName,
context.read<BoardBloc>().generateGroupNameFromGroup(group),
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -11,7 +11,7 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@@ -2,8 +2,8 @@ import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
class MobileEditorScreen extends StatelessWidget {
const MobileEditorScreen({
class MobileDocumentScreen extends StatelessWidget {
const MobileDocumentScreen({
super.key,
required this.id,
this.title,
@@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -16,22 +16,22 @@ class MobileFavoritePageFolder extends StatelessWidget {
const MobileFavoritePageFolder({
super.key,
required this.userProfile,
required this.workspaceSetting,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting;
final String workspaceId;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SidebarRootViewsBloc()
create: (_) => SidebarSectionsBloc()
..add(
SidebarRootViewsEvent.initial(
SidebarSectionsEvent.initial(
userProfile,
workspaceSetting.workspaceId,
workspaceId,
),
),
),
@@ -39,45 +39,52 @@ class MobileFavoritePageFolder extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(
builder: (context) {
final favoriteState = context.watch<FavoriteBloc>().state;
if (favoriteState.views.isEmpty) {
return FlowyMobileStateContainer.info(
emoji: '😁',
title: LocaleKeys.favorite_noFavorite.tr(),
description: LocaleKeys.favorite_noFavoriteHintText.tr(),
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<FavoriteBloc>().add(
const FavoriteEvent.initial(),
);
}
return Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SlidableAutoCloseBehavior(
child: Column(
children: [
MobileFavoriteFolder(
showHeader: false,
forceExpanded: true,
views: favoriteState.views,
),
const VSpace(100.0),
],
},
child: MultiBlocListener(
listeners: [
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(
builder: (context) {
final favoriteState = context.watch<FavoriteBloc>().state;
if (favoriteState.views.isEmpty) {
return FlowyMobileStateContainer.info(
emoji: '😁',
title: LocaleKeys.favorite_noFavorite.tr(),
description: LocaleKeys.favorite_noFavoriteHintText.tr(),
);
}
return Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SlidableAutoCloseBehavior(
child: Column(
children: [
MobileFavoriteFolder(
showHeader: false,
forceExpanded: true,
views: favoriteState.views,
),
const VSpace(100.0),
],
),
),
),
),
),
);
},
);
},
),
),
),
);
@@ -4,11 +4,13 @@ import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dar
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileFavoriteScreen extends StatelessWidget {
const MobileFavoriteScreen({
@@ -50,9 +52,23 @@ class MobileFavoriteScreen extends StatelessWidget {
return Scaffold(
body: SafeArea(
child: MobileFavoritePage(
userProfile: userProfile,
workspaceSetting: workspaceSetting,
child: BlocProvider(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
builder: (context, state) {
return MobileFavoritePage(
userProfile: userProfile,
workspaceId: state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
);
},
),
),
),
);
@@ -65,11 +81,11 @@ class MobileFavoritePage extends StatelessWidget {
const MobileFavoritePage({
super.key,
required this.userProfile,
required this.workspaceSetting,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceSettingPB workspaceSetting;
final String workspaceId;
@override
Widget build(BuildContext context) {
@@ -92,7 +108,7 @@ class MobileFavoritePage extends StatelessWidget {
Expanded(
child: MobileFavoritePageFolder(
userProfile: userProfile,
workspaceSetting: workspaceSetting,
workspaceId: workspaceId,
),
),
],
@@ -1,24 +1,28 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
// Contains Public And Private Sections
class MobileFolders extends StatelessWidget {
const MobileFolders({
super.key,
required this.user,
required this.workspaceSetting,
required this.workspaceId,
required this.showFavorite,
});
final UserProfilePB user;
final WorkspaceSettingPB workspaceSetting;
final String workspaceId;
final bool showFavorite;
@override
@@ -26,11 +30,11 @@ class MobileFolders extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SidebarRootViewsBloc()
create: (_) => SidebarSectionsBloc()
..add(
SidebarRootViewsEvent.initial(
SidebarSectionsEvent.initial(
user,
workspaceSetting.workspaceId,
workspaceId,
),
),
),
@@ -38,24 +42,51 @@ class MobileFolders extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(
builder: (context) {
final menuState = context.watch<SidebarRootViewsBloc>().state;
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.initial(
user,
state.currentWorkspace?.workspaceId ?? workspaceId,
),
);
},
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) {
final lastCreatedRootView = state.lastCreatedRootView;
if (lastCreatedRootView != null) {
context.pushView(lastCreatedRootView);
}
},
builder: (context, state) {
final isCollaborativeWorkspace =
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
return SlidableAutoCloseBehavior(
child: Column(
children: [
MobilePersonalFolder(
views: menuState.views,
),
...isCollaborativeWorkspace
? [
MobileSectionFolder(
title: LocaleKeys.sideBar_workspace.tr(),
categoryType: FolderCategoryType.public,
views: state.section.publicViews,
),
const VSpace(8.0),
MobileSectionFolder(
title: LocaleKeys.sideBar_private.tr(),
categoryType: FolderCategoryType.private,
views: state.section.privateViews,
),
]
: [
MobileSectionFolder(
title: LocaleKeys.sideBar_personal.tr(),
categoryType: FolderCategoryType.public,
views: state.section.publicViews,
),
],
const VSpace(8.0),
],
),
@@ -8,6 +8,7 @@ import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
@@ -15,6 +16,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
@@ -82,55 +84,73 @@ class MobileHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Header
Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: Platform.isAndroid ? 8.0 : 0.0,
),
child: MobileHomePageHeader(
userProfile: userProfile,
),
return BlocProvider(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
),
const Divider(),
// Folder
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Recent files
const MobileRecentFolder(),
// Folders
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: MobileFolders(
user: userProfile,
workspaceSetting: workspaceSetting,
showFavorite: false,
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: _TrashButton(),
),
],
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
builder: (context, state) {
if (state.currentWorkspace == null) {
return const SizedBox.shrink();
}
return Column(
children: [
// Header
Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: Platform.isAndroid ? 8.0 : 0.0,
),
child: MobileHomePageHeader(
userProfile: userProfile,
),
),
),
),
),
],
const Divider(),
// Folder
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Recent files
const MobileRecentFolder(),
// Folders
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: MobileFolders(
user: userProfile,
workspaceId:
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
showFavorite: false,
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: _TrashButton(),
),
],
),
),
),
),
),
],
);
},
),
);
}
}
@@ -1,12 +1,16 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -14,7 +18,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileHomePageHeader extends StatelessWidget {
const MobileHomePageHeader({super.key, required this.userProfile});
const MobileHomePageHeader({
super.key,
required this.userProfile,
});
final UserProfilePB userProfile;
@@ -25,33 +32,22 @@ class MobileHomePageHeader extends StatelessWidget {
..add(const SettingsUserEvent.initial()),
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
builder: (context, state) {
final userIcon = state.userProfile.iconUrl;
final isCollaborativeWorkspace =
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_UserIcon(userIcon: userIcon),
const HSpace(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const FlowyText.medium('AppFlowy', fontSize: 18),
const VSpace(4),
FlowyText.regular(
userProfile.email.isNotEmpty
? state.userProfile.email
: state.userProfile.name,
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
overflow: TextOverflow.ellipsis,
),
],
),
child: isCollaborativeWorkspace
? _MobileWorkspace(userProfile: userProfile)
: _MobileUser(userProfile: userProfile),
),
IconButton(
onPressed: () =>
context.push(MobileHomeSettingPage.routeName),
onPressed: () => context.push(
MobileHomeSettingPage.routeName,
),
icon: const FlowySvg(FlowySvgs.m_setting_m),
),
],
@@ -63,6 +59,154 @@ class MobileHomePageHeader extends StatelessWidget {
}
}
class _MobileUser extends StatelessWidget {
const _MobileUser({
required this.userProfile,
});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
final userIcon = userProfile.iconUrl;
return Row(
children: [
_UserIcon(userIcon: userIcon),
const HSpace(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const FlowyText.medium('AppFlowy', fontSize: 18),
const VSpace(4),
FlowyText.regular(
userProfile.email.isNotEmpty
? userProfile.email
: userProfile.name,
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
}
class _MobileWorkspace extends StatelessWidget {
const _MobileWorkspace({
required this.userProfile,
});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
final currentWorkspace = state.currentWorkspace;
if (currentWorkspace == null) {
return const SizedBox.shrink();
}
return GestureDetector(
onTap: () {
context.read<UserWorkspaceBloc>().add(
const UserWorkspaceEvent.fetchWorkspaces(),
);
_showSwitchWorkspacesBottomSheet(context);
},
child: Row(
children: [
const HSpace(2.0),
SizedBox.square(
dimension: 34.0,
child: WorkspaceIcon(
workspace: currentWorkspace,
iconSize: 26,
enableEdit: false,
),
),
const HSpace(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
FlowyText.medium(
currentWorkspace.name,
fontSize: 16.0,
overflow: TextOverflow.ellipsis,
),
const HSpace(4.0),
const FlowySvg(FlowySvgs.list_dropdown_s),
],
),
FlowyText.medium(
userProfile.email.isNotEmpty
? userProfile.email
: userProfile.name,
overflow: TextOverflow.ellipsis,
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
),
],
),
),
],
),
);
},
);
}
void _showSwitchWorkspacesBottomSheet(
BuildContext context,
) {
showMobileBottomSheet(
context,
showDivider: false,
showHeader: true,
showDragHandle: true,
title: LocaleKeys.workspace_menuTitle.tr(),
builder: (_) {
return BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
final currentWorkspace = state.currentWorkspace;
final workspaces = state.workspaces;
if (currentWorkspace == null || workspaces.isEmpty) {
return const SizedBox.shrink();
}
return MobileWorkspaceMenu(
userProfile: userProfile,
currentWorkspace: currentWorkspace,
workspaces: workspaces,
onWorkspaceSelected: (workspace) {
context.pop();
if (workspace == currentWorkspace) {
return;
}
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.openWorkspace(
workspace.workspaceId,
),
);
},
);
},
),
);
},
);
}
}
class _UserIcon extends StatelessWidget {
const _UserIcon({
required this.userIcon,
@@ -1,11 +1,16 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/recent/prelude.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileRecentFolder extends StatefulWidget {
const MobileRecentFolder({super.key});
@@ -22,31 +27,38 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
..add(
const RecentViewsEvent.initial(),
),
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
final ids = <String>{};
List<ViewPB> recentViews = state.views.reversed.toList();
recentViews.retainWhere((element) => ids.add(element.id));
// only keep the first 20 items.
recentViews = recentViews.take(20).toList();
if (recentViews.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
_RecentViews(
key: ValueKey(recentViews),
// the recent views are in reverse order
recentViews: recentViews,
),
const VSpace(12.0),
],
);
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<RecentViewsBloc>().add(
const RecentViewsEvent.fetchRecentViews(),
);
},
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
final ids = <String>{};
List<ViewPB> recentViews = state.views.reversed.toList();
recentViews.retainWhere((element) => ids.add(element.id));
// only keep the first 20 items.
recentViews = recentViews.take(20).toList();
if (recentViews.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
_RecentViews(
key: ValueKey(recentViews),
// the recent views are in reverse order
recentViews: recentViews,
),
const VSpace(12.0),
],
);
},
),
),
);
}
@@ -68,11 +80,70 @@ class _RecentViews extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: FlowyText.semibold(
LocaleKeys.sideBar_recent.tr(),
fontSize: 20.0,
child: GestureDetector(
child: FlowyText.semibold(
LocaleKeys.sideBar_recent.tr(),
fontSize: 20.0,
),
onTap: () {
showMobileBottomSheet(
context,
showDivider: false,
showDragHandle: true,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) {
return Column(
children: [
FlowyOptionTile.text(
text: LocaleKeys.button_clear.tr(),
leftIcon: FlowySvg(
FlowySvgs.m_delete_s,
color: Theme.of(context).colorScheme.error,
),
textColor: Theme.of(context).colorScheme.error,
onTap: () {
context.read<RecentViewsBloc>().add(
RecentViewsEvent.removeRecentViews(
recentViews.map((e) => e.id).toList(),
),
);
context.pop();
},
),
],
);
},
);
},
),
),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 24),
// child: FlowyText.semibold(
// LocaleKeys.sideBar_recent.tr(),
// fontSize: 20.0,
// ),
// ),
// if (kDebugMode)
// Padding(
// padding: const EdgeInsets.only(right: 16.0),
// child: FlowyButton(
// useIntrinsicWidth: true,
// text: FlowyText(LocaleKeys.button_clear.tr()),
// onTap: () {
// context.read<RecentViewsBloc>().add(
// RecentViewsEvent.removeRecentViews(
// recentViews.map((e) => e.id).toList(),
// ),
// );
// },
// ),
// ),
// ],
// ),
SingleChildScrollView(
key: const PageStorageKey('recent_views_page_storage_key'),
scrollDirection: Axis.horizontal,
@@ -3,9 +3,9 @@ import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_listener.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -53,7 +53,7 @@ class _MobileRecentViewState extends State<MobileRecentView> {
documentListener = DocumentListener(id: view.id)
..start(
didReceiveUpdate: (document) {
onDocEventUpdate: (document) {
setState(() {
view = view;
});
@@ -1,26 +1,33 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobilePersonalFolder extends StatelessWidget {
const MobilePersonalFolder({
class MobileSectionFolder extends StatelessWidget {
const MobileSectionFolder({
super.key,
required this.title,
required this.views,
required this.categoryType,
});
final String title;
final List<ViewPB> views;
final FolderCategoryType categoryType;
@override
Widget build(BuildContext context) {
return BlocProvider<FolderBloc>(
create: (context) => FolderBloc(type: FolderCategoryType.personal)
create: (context) => FolderBloc(type: categoryType)
..add(
const FolderEvent.initial(),
),
@@ -28,14 +35,25 @@ class MobilePersonalFolder extends StatelessWidget {
builder: (context, state) {
return Column(
children: [
MobilePersonalFolderHeader(
MobileSectionFolderHeader(
title: title,
isExpanded: context.read<FolderBloc>().state.isExpanded,
onPressed: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
onAdded: () => context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
),
onAdded: () {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name:
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: categoryType.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
);
},
),
const VSpace(8.0),
const Divider(
@@ -45,9 +63,9 @@ class MobilePersonalFolder extends StatelessWidget {
...views.map(
(view) => MobileViewItem(
key: ValueKey(
'${FolderCategoryType.personal.name} ${view.id}',
'${FolderCategoryType.private.name} ${view.id}',
),
categoryType: FolderCategoryType.personal,
categoryType: categoryType,
isFirstChild: view.id == views.first.id,
view: view,
level: 0,
@@ -1,30 +1,30 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobilePersonalFolderHeader extends StatefulWidget {
const MobilePersonalFolderHeader({
@visibleForTesting
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
class MobileSectionFolderHeader extends StatefulWidget {
const MobileSectionFolderHeader({
super.key,
required this.title,
required this.onPressed,
required this.onAdded,
required this.isExpanded,
});
final String title;
final VoidCallback onPressed;
final VoidCallback onAdded;
final bool isExpanded;
@override
State<MobilePersonalFolderHeader> createState() =>
_MobilePersonalFolderHeaderState();
State<MobileSectionFolderHeader> createState() =>
_MobileSectionFolderHeaderState();
}
class _MobilePersonalFolderHeaderState
extends State<MobilePersonalFolderHeader> {
class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
double _turns = 0;
@override
@@ -35,7 +35,7 @@ class _MobilePersonalFolderHeaderState
Expanded(
child: FlowyButton(
text: FlowyText.semibold(
LocaleKeys.sideBar_personal.tr(),
widget.title,
fontSize: 20.0,
),
margin: const EdgeInsets.symmetric(vertical: 8),
@@ -58,6 +58,7 @@ class _MobilePersonalFolderHeaderState
),
),
FlowyIconButton(
key: mobileCreateNewPageButtonKey,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
iconPadding: const EdgeInsets.all(2),
height: iconSize,
@@ -66,14 +67,7 @@ class _MobilePersonalFolderHeaderState
FlowySvgs.add_s,
size: Size.square(iconSize),
),
onPressed: () {
context.read<SidebarRootViewsBloc>().add(
SidebarRootViewsEvent.createRootView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
),
);
},
onPressed: widget.onAdded,
),
],
);
@@ -0,0 +1,121 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Only works on mobile.
class MobileWorkspaceMenu extends StatelessWidget {
const MobileWorkspaceMenu({
super.key,
required this.userProfile,
required this.currentWorkspace,
required this.workspaces,
required this.onWorkspaceSelected,
});
final UserProfilePB userProfile;
final UserWorkspacePB currentWorkspace;
final List<UserWorkspacePB> workspaces;
final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
@override
Widget build(BuildContext context) {
final List<Widget> children = [];
for (var i = 0; i < workspaces.length; i++) {
final workspace = workspaces[i];
children.add(
_WorkspaceMenuItem(
key: ValueKey(workspace.workspaceId),
userProfile: userProfile,
workspace: workspace,
showTopBorder: i == 0,
currentWorkspace: currentWorkspace,
onWorkspaceSelected: onWorkspaceSelected,
),
);
}
return Column(
children: children,
);
}
}
class _WorkspaceMenuItem extends StatelessWidget {
const _WorkspaceMenuItem({
super.key,
required this.userProfile,
required this.workspace,
required this.showTopBorder,
required this.currentWorkspace,
required this.onWorkspaceSelected,
});
final UserProfilePB userProfile;
final UserWorkspacePB workspace;
final bool showTopBorder;
final UserWorkspacePB currentWorkspace;
final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => WorkspaceMemberBloc(
userProfile: userProfile,
workspace: workspace,
)..add(const WorkspaceMemberEvent.initial()),
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
builder: (context, state) {
final members = state.members;
return FlowyOptionTile.text(
content: Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowyText(
workspace.name,
fontSize: 14,
fontWeight: FontWeight.w500,
),
FlowyText(
state.isLoading
? ''
: LocaleKeys.settings_appearance_members_membersCount
.plural(
members.length,
),
fontSize: 10.0,
color: Theme.of(context).hintColor,
),
],
),
),
),
height: 60,
showTopBorder: showTopBorder,
leftIcon: WorkspaceIcon(
enableEdit: false,
iconSize: 26,
workspace: workspace,
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
onTap: () => onWorkspaceSelected(workspace),
);
},
),
);
}
}
@@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
@@ -80,15 +80,15 @@ class _NotificationScreenContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => SidebarRootViewsBloc()
create: (_) => SidebarSectionsBloc()
..add(
SidebarRootViewsEvent.initial(
SidebarSectionsEvent.initial(
userProfile,
workspaceSetting.workspaceId,
),
),
child: BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
builder: (context, menuState) =>
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, sectionState) =>
BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
builder: (context, filterState) =>
BlocBuilder<ReminderBloc, ReminderState>(
@@ -122,7 +122,7 @@ class _NotificationScreenContent extends StatelessWidget {
NotificationsView(
shownReminders: pastReminders,
reminderBloc: reminderBloc,
views: menuState.views,
views: sectionState.section.publicViews,
onAction: _onAction,
onDelete: _onDelete,
onReadChanged: _onReadChanged,
@@ -134,7 +134,7 @@ class _NotificationScreenContent extends StatelessWidget {
NotificationsView(
shownReminders: upcomingReminders,
reminderBloc: reminderBloc,
views: menuState.views,
views: sectionState.section.publicViews,
isUpcoming: true,
onAction: _onAction,
),
@@ -406,6 +406,10 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
ViewEvent.createView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout,
section:
widget.categoryType != FolderCategoryType.favorite
? widget.categoryType.toViewSectionPB
: null,
),
);
},
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/device_info_task.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../widgets/widgets.dart';
@@ -32,10 +34,20 @@ class AboutSettingGroup extends StatelessWidget {
),
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
),
if (kDebugMode)
MobileSettingItem(
name: 'Feature Flags',
trailing: const Icon(
Icons.chevron_right,
),
onTap: () {
context.push(FeatureFlagScreen.routeName);
},
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_version.tr(),
trailing: FlowyText(
'${DeviceOrApplicationInfoTask.applicationVersion} (${DeviceOrApplicationInfoTask.buildNumber})',
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
color: Theme.of(context).colorScheme.onSurface,
),
),
@@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class SelfHostUrlBottomSheet extends StatefulWidget {
const SelfHostUrlBottomSheet({
@@ -38,32 +37,9 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
LocaleKeys.editor_urlHint.tr(),
style: theme.textTheme.labelSmall,
),
IconButton(
icon: Icon(
Icons.close,
color: theme.hintColor,
),
onPressed: () {
context.pop();
},
),
],
),
const SizedBox(
height: 16,
),
Form(
key: _formKey,
child: TextFormField(
@@ -36,6 +36,14 @@ class _SelfHostSettingGroupState extends State<SelfHostSettingGroup> {
onTap: () {
showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.editor_urlHint.tr(),
showCloseButton: true,
showDivider: false,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
builder: (_) {
return SelfHostUrlBottomSheet(
url: url,
@@ -43,6 +43,7 @@ class MobileSettingItem extends StatelessWidget {
trailing: trailing,
onTap: onTap,
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.only(left: 8.0),
),
);
}
@@ -36,26 +36,31 @@ class FlowyOptionTile extends StatelessWidget {
this.content,
this.backgroundColor,
this.fontFamily,
this.height,
});
factory FlowyOptionTile.text({
required String text,
String? text,
Widget? content,
Color? textColor,
bool showTopBorder = true,
bool showBottomBorder = true,
Widget? leftIcon,
Widget? trailing,
VoidCallback? onTap,
double? height,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
text: text,
content: content,
textColor: textColor,
onTap: onTap,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: trailing,
height: height,
);
}
@@ -174,6 +179,8 @@ class FlowyOptionTile extends StatelessWidget {
final Color? backgroundColor;
final String? fontFamily;
final double? height;
@override
Widget build(BuildContext context) {
final leadingWidget = _buildLeading();
@@ -182,16 +189,19 @@ class FlowyOptionTile extends StatelessWidget {
color: backgroundColor,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
if (leadingWidget != null) leadingWidget,
if (content != null) content!,
if (content == null) _buildText(),
if (content == null) _buildTextField(),
if (trailing != null) trailing!,
],
child: SizedBox(
height: height,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
if (leadingWidget != null) leadingWidget,
if (content != null) content!,
if (content == null) _buildText(),
if (content == null) _buildTextField(),
if (trailing != null) trailing!,
],
),
),
),
);
@@ -1,10 +1,14 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -35,12 +39,14 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
(event, emit) async {
await event.when(
didUpdateCell: (RelationCellDataPB? cellData) async {
if (cellData == null || cellData.rowIds.isEmpty) {
if (cellData == null ||
cellData.rowIds.isEmpty ||
state.relatedDatabaseMeta == null) {
emit(state.copyWith(rows: const []));
return;
}
final payload = RepeatedRowIdPB(
databaseId: state.relatedDatabaseId,
databaseId: state.relatedDatabaseMeta!.databaseId,
rowIds: cellData.rowIds,
);
final result =
@@ -54,8 +60,16 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
);
emit(state.copyWith(rows: rows));
},
didUpdateRelationDatabaseId: (databaseId) {
emit(state.copyWith(relatedDatabaseId: databaseId));
didUpdateRelationTypeOption: (typeOption) async {
if (typeOption.databaseId.isEmpty) {
return;
}
final meta = await _loadDatabaseMeta(typeOption.databaseId);
emit(state.copyWith(relatedDatabaseMeta: meta));
_loadCellData();
},
selectDatabaseId: (databaseId) async {
await _updateTypeOption(databaseId);
},
selectRow: (rowId) async {
await _handleSelectRow(rowId);
@@ -73,29 +87,30 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
}
},
onCellFieldChanged: (field) {
if (!isClosed) {
// hack: SingleFieldListener receives notification before
// FieldController's copy is updated.
Future.delayed(const Duration(milliseconds: 50), () {
// hack: SingleFieldListener receives notification before
// FieldController's copy is updated.
Future.delayed(const Duration(milliseconds: 50), () {
if (!isClosed) {
final RelationTypeOptionPB typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser());
add(
RelationCellEvent.didUpdateRelationDatabaseId(
typeOption.databaseId,
),
);
});
}
add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
}
});
},
);
}
void _init() {
final RelationTypeOptionPB typeOption =
final typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser());
add(RelationCellEvent.didUpdateRelationDatabaseId(typeOption.databaseId));
add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
}
void _loadCellData() {
final cellData = cellController.getCellData();
add(RelationCellEvent.didUpdateCell(cellData));
if (!isClosed) {
add(RelationCellEvent.didUpdateCell(cellData));
}
}
Future<void> _handleSelectRow(String rowId) async {
@@ -115,25 +130,66 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
final result = await DatabaseEventUpdateRelationCell(payload).send();
result.fold((l) => null, (err) => Log.error(err));
}
Future<DatabaseMeta?> _loadDatabaseMeta(String databaseId) async {
final getDatabaseResult = await DatabaseEventGetDatabases().send();
final databaseMeta = getDatabaseResult.fold<DatabaseMetaPB?>(
(s) => s.items.firstWhereOrNull(
(metaPB) => metaPB.databaseId == databaseId,
),
(f) => null,
);
if (databaseMeta != null) {
final result =
await ViewBackendService.getView(databaseMeta.inlineViewId);
return result.fold(
(s) => DatabaseMeta(
databaseId: databaseId,
inlineViewId: databaseMeta.inlineViewId,
databaseName: s.name,
),
(f) => null,
);
}
return null;
}
Future<void> _updateTypeOption(String databaseId) async {
final newDateTypeOption = RelationTypeOptionPB(
databaseId: databaseId,
);
final result = await FieldBackendService.updateFieldTypeOption(
viewId: cellController.viewId,
fieldId: cellController.fieldInfo.id,
typeOptionData: newDateTypeOption.writeToBuffer(),
);
result.fold((s) => null, (err) => Log.error(err));
}
}
@freezed
class RelationCellEvent with _$RelationCellEvent {
const factory RelationCellEvent.didUpdateRelationDatabaseId(
String databaseId,
) = _DidUpdateRelationDatabaseId;
const factory RelationCellEvent.didUpdateRelationTypeOption(
RelationTypeOptionPB typeOption,
) = _DidUpdateRelationTypeOption;
const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) =
_DidUpdateCell;
const factory RelationCellEvent.selectDatabaseId(
String databaseId,
) = _SelectDatabaseId;
const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId;
}
@freezed
class RelationCellState with _$RelationCellState {
const factory RelationCellState({
required String relatedDatabaseId,
required DatabaseMeta? relatedDatabaseMeta,
required List<RelatedRowDataPB> rows,
}) = _RelationCellState;
factory RelationCellState.initial() =>
const RelationCellState(relatedDatabaseId: "", rows: []);
factory RelationCellState.initial() => const RelationCellState(
relatedDatabaseMeta: null,
rows: [],
);
}
@@ -0,0 +1,433 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'select_option_cell_editor_bloc.freezed.dart';
const String createSelectOptionSuggestionId =
"create_select_option_suggestion_id";
class SelectOptionCellEditorBloc
extends Bloc<SelectOptionCellEditorEvent, SelectOptionCellEditorState> {
SelectOptionCellEditorBloc({required this.cellController})
: _selectOptionService = SelectOptionCellBackendService(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
rowId: cellController.rowId,
),
_typeOptionAction = cellController.fieldType == FieldType.SingleSelect
? SingleSelectAction(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
onTypeOptionUpdated: (typeOptionData) =>
FieldBackendService.updateFieldTypeOption(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
typeOptionData: typeOptionData,
),
)
: MultiSelectAction(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
onTypeOptionUpdated: (typeOptionData) =>
FieldBackendService.updateFieldTypeOption(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
typeOptionData: typeOptionData,
),
),
super(SelectOptionCellEditorState.initial(cellController)) {
_dispatch();
_startListening();
_loadOptions();
}
final SelectOptionCellBackendService _selectOptionService;
final ISelectOptionAction _typeOptionAction;
final SelectOptionCellController cellController;
VoidCallback? _onCellChangedFn;
final List<SelectOptionPB> allOptions = [];
String filter = "";
void _dispatch() {
on<SelectOptionCellEditorEvent>(
(event, emit) async {
await event.when(
didReceiveOptions: (options, selectedOptions) {
final result = _getVisibleOptions(options);
allOptions
..clear()
..addAll(options);
emit(
state.copyWith(
options: result.options,
createSelectOptionSuggestion:
result.createSelectOptionSuggestion,
selectedOptions: selectedOptions,
),
);
},
createOption: () async {
if (state.createSelectOptionSuggestion == null) {
return;
}
filter = "";
await _createOption(
name: state.createSelectOptionSuggestion!.name,
color: state.createSelectOptionSuggestion!.color,
);
emit(state.copyWith(clearFilter: true));
},
deleteOption: (option) async {
await _deleteOption([option]);
},
deleteAllOptions: () async {
if (allOptions.isNotEmpty) {
await _deleteOption(allOptions);
}
},
updateOption: (option) async {
await _updateOption(option);
},
selectOption: (optionId) async {
await _selectOptionService.select(optionIds: [optionId]);
},
unSelectOption: (optionId) async {
await _selectOptionService.unSelect(optionIds: [optionId]);
},
unSelectLastOption: () async {
if (state.selectedOptions.isEmpty) {
return;
}
final lastSelectedOptionId = state.selectedOptions.last.id;
await _selectOptionService
.unSelect(optionIds: [lastSelectedOptionId]);
},
submitTextField: () {
_submitTextFieldValue(emit);
},
selectMultipleOptions: (optionNames, remainder) {
if (optionNames.isNotEmpty) {
_selectMultipleOptions(optionNames);
}
_filterOption(remainder, emit);
},
reorderOption: (fromOptionId, toOptionId) {
final options = _typeOptionAction.reorderOption(
allOptions,
fromOptionId,
toOptionId,
);
allOptions
..clear()
..addAll(options);
final result = _getVisibleOptions(options);
emit(state.copyWith(options: result.options));
},
filterOption: (filterText) {
_filterOption(filterText, emit);
},
focusPreviousOption: () {
_focusOption(true, emit);
},
focusNextOption: () {
_focusOption(false, emit);
},
updateFocusedOption: (optionId) {
emit(state.copyWith(focusedOptionId: optionId));
},
resetClearFilterFlag: () {
emit(state.copyWith(clearFilter: false));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
return super.close();
}
Future<void> _createOption({
required String name,
required SelectOptionColorPB color,
}) async {
final result = await _selectOptionService.create(
name: name,
color: color,
);
result.fold((l) => {}, (err) => Log.error(err));
}
Future<void> _deleteOption(List<SelectOptionPB> options) async {
final result = await _selectOptionService.delete(options: options);
result.fold((l) => null, (err) => Log.error(err));
}
Future<void> _updateOption(SelectOptionPB option) async {
final result = await _selectOptionService.update(
option: option,
);
result.fold((l) => null, (err) => Log.error(err));
}
void _submitTextFieldValue(Emitter<SelectOptionCellEditorState> emit) {
if (state.focusedOptionId == null) {
return;
}
final focusedOptionId = state.focusedOptionId!;
if (focusedOptionId == createSelectOptionSuggestionId) {
filter = "";
_createOption(
name: state.createSelectOptionSuggestion!.name,
color: state.createSelectOptionSuggestion!.color,
);
emit(
state.copyWith(
createSelectOptionSuggestion: null,
clearFilter: true,
),
);
} else if (!state.selectedOptions
.any((option) => option.id == focusedOptionId)) {
_selectOptionService.select(optionIds: [focusedOptionId]);
}
}
void _selectMultipleOptions(List<String> optionNames) {
final optionIds = optionNames
.map(
(name) => allOptions.firstWhereOrNull(
(option) => option.name.toLowerCase() == name.toLowerCase(),
),
)
.nonNulls
.map((option) => option.id)
.toList();
_selectOptionService.select(optionIds: optionIds);
}
void _filterOption(
String filterText,
Emitter<SelectOptionCellEditorState> emit,
) {
filter = filterText;
final _MakeOptionResult result = _getVisibleOptions(
allOptions,
);
final focusedOptionId = result.options.isEmpty
? result.createSelectOptionSuggestion == null
? null
: createSelectOptionSuggestionId
: result.options.any((option) => option.id == state.focusedOptionId)
? state.focusedOptionId
: result.options.first.id;
emit(
state.copyWith(
options: result.options,
createSelectOptionSuggestion: result.createSelectOptionSuggestion,
focusedOptionId: focusedOptionId,
),
);
}
Future<void> _loadOptions() async {
final result = await _selectOptionService.getCellData();
if (isClosed) {
Log.warn("Unexpecteded closing the bloc");
return;
}
return result.fold(
(data) => add(
SelectOptionCellEditorEvent.didReceiveOptions(
data.options,
data.selectOptions,
),
),
(err) {
Log.error(err);
return null;
},
);
}
_MakeOptionResult _getVisibleOptions(
List<SelectOptionPB> allOptions,
) {
final List<SelectOptionPB> options = List.from(allOptions);
String newOptionName = filter;
if (filter.isNotEmpty) {
options.retainWhere((option) {
final name = option.name.toLowerCase();
final lFilter = filter.toLowerCase();
if (name == lFilter) {
newOptionName = "";
}
return name.contains(lFilter);
});
}
return _MakeOptionResult(
options: options,
createSelectOptionSuggestion: newOptionName.isEmpty
? null
: CreateSelectOptionSuggestion(
name: newOptionName,
color: newSelectOptionColor(allOptions),
),
);
}
void _focusOption(bool previous, Emitter<SelectOptionCellEditorState> emit) {
if (state.options.isEmpty && state.createSelectOptionSuggestion == null) {
return;
}
final optionIds = [
...state.options.map((e) => e.id),
if (state.createSelectOptionSuggestion != null)
createSelectOptionSuggestionId,
];
if (state.focusedOptionId == null) {
emit(
state.copyWith(
focusedOptionId: previous ? optionIds.last : optionIds.first,
),
);
return;
}
final currentIndex =
optionIds.indexWhere((id) => id == state.focusedOptionId);
final newIndex = currentIndex == -1
? 0
: (currentIndex + (previous ? -1 : 1)) % optionIds.length;
emit(state.copyWith(focusedOptionId: optionIds[newIndex]));
}
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (selectOptionContext) {
_loadOptions();
},
onCellFieldChanged: (field) {
_loadOptions();
},
);
}
}
@freezed
class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent {
const factory SelectOptionCellEditorEvent.didReceiveOptions(
List<SelectOptionPB> options,
List<SelectOptionPB> selectedOptions,
) = _DidReceiveOptions;
const factory SelectOptionCellEditorEvent.createOption() = _CreateOption;
const factory SelectOptionCellEditorEvent.selectOption(String optionId) =
_SelectOption;
const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) =
_UnSelectOption;
const factory SelectOptionCellEditorEvent.unSelectLastOption() =
_UnSelectLastOption;
const factory SelectOptionCellEditorEvent.updateOption(
SelectOptionPB option,
) = _UpdateOption;
const factory SelectOptionCellEditorEvent.deleteOption(
SelectOptionPB option,
) = _DeleteOption;
const factory SelectOptionCellEditorEvent.deleteAllOptions() =
_DeleteAllOptions;
const factory SelectOptionCellEditorEvent.reorderOption(
String fromOptionId,
String toOptionId,
) = _ReorderOption;
const factory SelectOptionCellEditorEvent.filterOption(String filterText) =
_SelectOptionFilter;
const factory SelectOptionCellEditorEvent.submitTextField() =
_SubmitTextField;
const factory SelectOptionCellEditorEvent.selectMultipleOptions(
List<String> optionNames,
String remainder,
) = _SelectMultipleOptions;
const factory SelectOptionCellEditorEvent.focusPreviousOption() =
_FocusPreviousOption;
const factory SelectOptionCellEditorEvent.focusNextOption() =
_FocusNextOption;
const factory SelectOptionCellEditorEvent.updateFocusedOption(
String? optionId,
) = _UpdateFocusedOption;
const factory SelectOptionCellEditorEvent.resetClearFilterFlag() =
_ResetClearFilterFlag;
}
@freezed
class SelectOptionCellEditorState with _$SelectOptionCellEditorState {
const factory SelectOptionCellEditorState({
required List<SelectOptionPB> options,
required List<SelectOptionPB> selectedOptions,
required CreateSelectOptionSuggestion? createSelectOptionSuggestion,
required String? focusedOptionId,
required bool clearFilter,
}) = _SelectOptionEditorState;
factory SelectOptionCellEditorState.initial(
SelectOptionCellController context,
) {
final data = context.getCellData(loadIfNotExist: false);
return SelectOptionCellEditorState(
options: data?.options ?? [],
selectedOptions: data?.selectOptions ?? [],
createSelectOptionSuggestion: null,
focusedOptionId: null,
clearFilter: false,
);
}
}
class _MakeOptionResult {
_MakeOptionResult({
required this.options,
required this.createSelectOptionSuggestion,
});
List<SelectOptionPB> options;
CreateSelectOptionSuggestion? createSelectOptionSuggestion;
}
class CreateSelectOptionSuggestion {
CreateSelectOptionSuggestion({
required this.name,
required this.color,
});
final String name;
final SelectOptionColorPB color;
}
@@ -1,318 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'select_option_editor_bloc.freezed.dart';
class SelectOptionCellEditorBloc
extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
SelectOptionCellEditorBloc({required this.cellController})
: _selectOptionService = SelectOptionCellBackendService(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
rowId: cellController.rowId,
),
super(SelectOptionEditorState.initial(cellController)) {
_dispatch();
}
final SelectOptionCellBackendService _selectOptionService;
final SelectOptionCellController cellController;
VoidCallback? _onCellChangedFn;
void _dispatch() {
on<SelectOptionEditorEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
await _loadOptions();
},
didReceiveOptions: (options, selectedOptions) {
final result = _makeOptions(state.filter, options);
emit(
state.copyWith(
allOptions: options,
options: result.options,
createOption: result.createOption,
selectedOptions: selectedOptions,
),
);
},
newOption: (optionName) async {
await _createOption(optionName);
emit(
state.copyWith(
filter: null,
),
);
},
deleteOption: (option) async {
await _deleteOption([option]);
},
deleteAllOptions: () async {
if (state.allOptions.isNotEmpty) {
await _deleteOption(state.allOptions);
}
},
updateOption: (option) async {
await _updateOption(option);
},
selectOption: (optionId) async {
await _selectOptionService.select(optionIds: [optionId]);
final selectedOption = [
...state.selectedOptions,
state.options.firstWhere(
(element) => element.id == optionId,
),
];
emit(
state.copyWith(
selectedOptions: selectedOption,
),
);
},
unSelectOption: (optionId) async {
await _selectOptionService.unSelect(optionIds: [optionId]);
final selectedOptions = [...state.selectedOptions]
..removeWhere((e) => e.id == optionId);
emit(
state.copyWith(
selectedOptions: selectedOptions,
),
);
},
trySelectOption: (optionName) {
_trySelectOption(optionName, emit);
},
selectMultipleOptions: (optionNames, remainder) {
if (optionNames.isNotEmpty) {
_selectMultipleOptions(optionNames);
}
_filterOption(remainder, emit);
},
filterOption: (optionName) {
_filterOption(optionName, emit);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
return super.close();
}
Future<void> _createOption(String name) async {
final result = await _selectOptionService.create(name: name);
result.fold((l) => {}, (err) => Log.error(err));
}
Future<void> _deleteOption(List<SelectOptionPB> options) async {
final result = await _selectOptionService.delete(options: options);
result.fold((l) => null, (err) => Log.error(err));
}
Future<void> _updateOption(SelectOptionPB option) async {
final result = await _selectOptionService.update(
option: option,
);
result.fold((l) => null, (err) => Log.error(err));
}
void _trySelectOption(
String optionName,
Emitter<SelectOptionEditorState> emit,
) {
SelectOptionPB? matchingOption;
bool optionExistsButSelected = false;
for (final option in state.options) {
if (option.name.toLowerCase() == optionName.toLowerCase()) {
if (!state.selectedOptions.contains(option)) {
matchingOption = option;
break;
} else {
optionExistsButSelected = true;
}
}
}
// if there isn't a matching option at all, then create it
if (matchingOption == null && !optionExistsButSelected) {
_createOption(optionName);
}
// if there is an unselected matching option, select it
if (matchingOption != null) {
_selectOptionService.select(optionIds: [matchingOption.id]);
}
// clear the filter
emit(state.copyWith(filter: null));
}
void _selectMultipleOptions(List<String> optionNames) {
// The options are unordered. So in order to keep the inserted [optionNames]
// order, it needs to get the option id in the [optionNames] order.
final lowerCaseNames = optionNames.map((e) => e.toLowerCase());
final Map<String, String> optionIdsMap = {};
for (final option in state.options) {
optionIdsMap[option.name.toLowerCase()] = option.id;
}
final optionIds = lowerCaseNames
.where((name) => optionIdsMap[name] != null)
.map((name) => optionIdsMap[name]!)
.toList();
_selectOptionService.select(optionIds: optionIds);
}
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
final _MakeOptionResult result = _makeOptions(
optionName,
state.allOptions,
);
emit(
state.copyWith(
filter: optionName,
options: result.options,
createOption: result.createOption,
),
);
}
Future<void> _loadOptions() async {
final result = await _selectOptionService.getCellData();
if (isClosed) {
Log.warn("Unexpecteded closing the bloc");
return;
}
return result.fold(
(data) => add(
SelectOptionEditorEvent.didReceiveOptions(
data.options,
data.selectOptions,
),
),
(err) {
Log.error(err);
return null;
},
);
}
_MakeOptionResult _makeOptions(
String? filter,
List<SelectOptionPB> allOptions,
) {
final List<SelectOptionPB> options = List.from(allOptions);
String? createOption = filter;
if (filter != null && filter.isNotEmpty) {
options.retainWhere((option) {
final name = option.name.toLowerCase();
final lFilter = filter.toLowerCase();
if (name == lFilter) {
createOption = null;
}
return name.contains(lFilter);
});
} else {
createOption = null;
}
return _MakeOptionResult(
options: options,
createOption: createOption,
);
}
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (selectOptionContext) {
_loadOptions();
},
onCellFieldChanged: (field) {
_loadOptions();
},
);
}
}
@freezed
class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
const factory SelectOptionEditorEvent.initial() = _Initial;
const factory SelectOptionEditorEvent.didReceiveOptions(
List<SelectOptionPB> options,
List<SelectOptionPB> selectedOptions,
) = _DidReceiveOptions;
const factory SelectOptionEditorEvent.newOption(String optionName) =
_NewOption;
const factory SelectOptionEditorEvent.selectOption(String optionId) =
_SelectOption;
const factory SelectOptionEditorEvent.unSelectOption(String optionId) =
_UnSelectOption;
const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) =
_UpdateOption;
const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) =
_DeleteOption;
const factory SelectOptionEditorEvent.deleteAllOptions() = _DeleteAllOptions;
const factory SelectOptionEditorEvent.filterOption(String optionName) =
_SelectOptionFilter;
const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
_TrySelectOption;
const factory SelectOptionEditorEvent.selectMultipleOptions(
List<String> optionNames,
String remainder,
) = _SelectMultipleOptions;
}
@freezed
class SelectOptionEditorState with _$SelectOptionEditorState {
const factory SelectOptionEditorState({
required List<SelectOptionPB> options,
required List<SelectOptionPB> allOptions,
required List<SelectOptionPB> selectedOptions,
required String? createOption,
required String? filter,
}) = _SelectOptionEditorState;
factory SelectOptionEditorState.initial(SelectOptionCellController context) {
final data = context.getCellData(loadIfNotExist: false);
return SelectOptionEditorState(
options: data?.options ?? [],
allOptions: data?.options ?? [],
selectedOptions: data?.selectOptions ?? [],
createOption: null,
filter: null,
);
}
}
class _MakeOptionResult {
_MakeOptionResult({
required this.options,
required this.createOption,
});
List<SelectOptionPB> options;
String? createOption;
}
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
@@ -29,15 +30,17 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
void _dispatch() {
on<URLCellEvent>(
(event, emit) async {
event.when(
await event.when(
initial: () {
_startListening();
},
didReceiveCellUpdate: (cellData) {
didReceiveCellUpdate: (cellData) async {
final content = cellData?.content ?? "";
final isValid = await isUrlValid(content);
emit(
state.copyWith(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
content: content,
isValid: isValid,
),
);
},
@@ -58,6 +61,31 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
},
);
}
Future<bool> isUrlValid(String content) async {
if (content.isEmpty) {
return true;
}
try {
// check protocol is provided
const linkPrefix = [
'http://',
'https://',
];
final shouldAddScheme =
!linkPrefix.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
// get hostname and check validity
final uri = Uri.parse(url);
final hostName = uri.host;
await InternetAddress.lookup(hostName);
} catch (_) {
return false;
}
return true;
}
}
@freezed
@@ -72,14 +100,14 @@ class URLCellEvent with _$URLCellEvent {
class URLCellState with _$URLCellState {
const factory URLCellState({
required String content,
required String url,
required bool isValid,
}) = _URLCellState;
factory URLCellState.initial(URLCellController context) {
final cellData = context.getCellData();
return URLCellState(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
isValid: true,
);
}
}
@@ -47,7 +47,7 @@ class FieldInfo with _$FieldInfo {
}
bool get canCreateFilter {
if (hasFilter) {
if (isGroupField) {
return false;
}
@@ -58,6 +58,7 @@ class FieldInfo with _$FieldInfo {
case FieldType.RichText:
case FieldType.SingleSelect:
case FieldType.Checklist:
case FieldType.URL:
return true;
default:
return false;
@@ -0,0 +1,63 @@
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'relation_type_option_cubit.freezed.dart';
class RelationDatabaseListCubit extends Cubit<RelationDatabaseListState> {
RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) {
_loadDatabaseMetas();
}
void _loadDatabaseMetas() async {
final getDatabaseResult = await DatabaseEventGetDatabases().send();
final metaPBs = getDatabaseResult.fold<List<DatabaseMetaPB>>(
(s) => s.items,
(f) => [],
);
final futures = metaPBs.map((meta) {
return ViewBackendService.getView(meta.inlineViewId).then(
(result) => result.fold(
(s) => DatabaseMeta(
databaseId: meta.databaseId,
inlineViewId: meta.inlineViewId,
databaseName: s.name,
),
(f) => null,
),
);
});
final databaseMetas = await Future.wait(futures);
emit(
RelationDatabaseListState(
databaseMetas: databaseMetas.nonNulls.toList(),
),
);
}
}
@freezed
class DatabaseMeta with _$DatabaseMeta {
factory DatabaseMeta({
/// id of the database
required String databaseId,
/// id of the inline view
required String inlineViewId,
/// name of the database, currently identical to the name of the inline view
required String databaseName,
}) = _DatabaseMeta;
}
@freezed
class RelationDatabaseListState with _$RelationDatabaseListState {
factory RelationDatabaseListState({
required List<DatabaseMeta> databaseMetas,
}) = _RelationDatabaseListState;
factory RelationDatabaseListState.initial() =>
RelationDatabaseListState(databaseMetas: []);
}
@@ -20,10 +20,10 @@ class SelectOptionTypeOptionBloc
void _dispatch() {
on<SelectOptionTypeOptionEvent>(
(event, emit) async {
await event.when(
createOption: (optionName) async {
event.when(
createOption: (optionName) {
final List<SelectOptionPB> options =
await typeOptionAction.insertOption(state.options, optionName);
typeOptionAction.insertOption(state.options, optionName);
emit(state.copyWith(options: options));
},
addingOption: () {
@@ -33,15 +33,23 @@ class SelectOptionTypeOptionBloc
emit(state.copyWith(isEditingOption: false, newOptionName: null));
},
updateOption: (option) {
final List<SelectOptionPB> options =
final options =
typeOptionAction.updateOption(state.options, option);
emit(state.copyWith(options: options));
},
deleteOption: (option) {
final List<SelectOptionPB> options =
final options =
typeOptionAction.deleteOption(state.options, option);
emit(state.copyWith(options: options));
},
reorderOption: (fromOptionId, toOptionId) {
final options = typeOptionAction.reorderOption(
state.options,
fromOptionId,
toOptionId,
);
emit(state.copyWith(options: options));
},
);
},
);
@@ -61,6 +69,10 @@ class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {
const factory SelectOptionTypeOptionEvent.deleteOption(
SelectOptionPB option,
) = _DeleteOption;
const factory SelectOptionTypeOptionEvent.reorderOption(
String fromOptionId,
String toOptionId,
) = _ReorderOption;
}
@freezed
@@ -1,9 +1,7 @@
import 'dart:async';
import 'package:appflowy/plugins/database/domain/type_option_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/builder.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:nanoid/nanoid.dart';
abstract class ISelectOptionAction {
ISelectOptionAction({
@@ -20,29 +18,25 @@ abstract class ISelectOptionAction {
onTypeOptionUpdated(newTypeOption.writeToBuffer());
}
Future<List<SelectOptionPB>> insertOption(
List<SelectOptionPB> insertOption(
List<SelectOptionPB> options,
String optionName,
) {
final newOptions = List<SelectOptionPB>.from(options);
return service.newOption(name: optionName).then((result) {
return result.fold(
(option) {
final exists =
newOptions.any((element) => element.name == option.name);
if (!exists) {
newOptions.insert(0, option);
}
if (options.any((element) => element.name == optionName)) {
return options;
}
updateTypeOption(newOptions);
return newOptions;
},
(err) {
Log.error(err);
return newOptions;
},
);
});
final newOptions = List<SelectOptionPB>.from(options);
final newSelectOption = SelectOptionPB()
..id = nanoid(4)
..color = newSelectOptionColor(options)
..name = optionName;
newOptions.insert(0, newSelectOption);
updateTypeOption(newOptions);
return newOptions;
}
List<SelectOptionPB> deleteOption(
@@ -73,6 +67,25 @@ abstract class ISelectOptionAction {
updateTypeOption(newOptions);
return newOptions;
}
List<SelectOptionPB> reorderOption(
List<SelectOptionPB> options,
String fromOptionId,
String toOptionId,
) {
final newOptions = List<SelectOptionPB>.from(options);
final fromIndex =
newOptions.indexWhere((element) => element.id == fromOptionId);
final toIndex =
newOptions.indexWhere((element) => element.id == toOptionId);
if (fromIndex != -1 && toIndex != -1) {
newOptions.insert(toIndex, newOptions.removeAt(fromIndex));
}
updateTypeOption(newOptions);
return newOptions;
}
}
class MultiSelectAction extends ISelectOptionAction {
@@ -102,3 +115,19 @@ class SingleSelectAction extends ISelectOptionAction {
onTypeOptionUpdated(newTypeOption.writeToBuffer());
}
}
SelectOptionColorPB newSelectOptionColor(List<SelectOptionPB> options) {
final colorFrequency = List.filled(SelectOptionColorPB.values.length, 0);
for (final option in options) {
colorFrequency[option.color.value]++;
}
final minIndex = colorFrequency
.asMap()
.entries
.reduce((a, b) => a.value <= b.value ? a : b)
.key;
return SelectOptionColorPB.valueOf(minIndex) ?? SelectOptionColorPB.Purple;
}
@@ -0,0 +1,112 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/sync/database_sync_state_listener.dart';
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'database_sync_bloc.freezed.dart';
class DatabaseSyncBloc extends Bloc<DatabaseSyncEvent, DatabaseSyncBlocState> {
DatabaseSyncBloc({
required this.view,
}) : super(DatabaseSyncBlocState.initial()) {
on<DatabaseSyncEvent>(
(event, emit) async {
await event.when(
initial: () async {
final userProfile = await getIt<AuthService>().getUser().then(
(value) => value.fold((s) => s, (f) => null),
);
final databaseId = await DatabaseViewBackendService(viewId: view.id)
.getDatabaseId()
.then((value) => value.fold((s) => s, (f) => null));
emit(
state.copyWith(
shouldShowIndicator: userProfile?.authenticator ==
AuthenticatorPB.AppFlowyCloud &&
databaseId != null,
),
);
if (databaseId != null) {
_syncStateListener =
DatabaseSyncStateListener(databaseId: databaseId)
..start(
didReceiveSyncState: (syncState) {
Log.info(
'database sync state changed, from ${state.syncState} to $syncState',
);
add(DatabaseSyncEvent.syncStateChanged(syncState));
},
);
}
final isNetworkConnected = await _connectivity
.checkConnectivity()
.then((value) => value != ConnectivityResult.none);
emit(state.copyWith(isNetworkConnected: isNetworkConnected));
connectivityStream =
_connectivity.onConnectivityChanged.listen((result) {
add(DatabaseSyncEvent.networkStateChanged(result));
});
},
syncStateChanged: (syncState) {
emit(state.copyWith(syncState: syncState.value));
},
networkStateChanged: (result) {
emit(
state.copyWith(
isNetworkConnected: result != ConnectivityResult.none,
),
);
},
);
},
);
}
final ViewPB view;
final _connectivity = Connectivity();
StreamSubscription? connectivityStream;
DatabaseSyncStateListener? _syncStateListener;
@override
Future<void> close() async {
await connectivityStream?.cancel();
await _syncStateListener?.stop();
return super.close();
}
}
@freezed
class DatabaseSyncEvent with _$DatabaseSyncEvent {
const factory DatabaseSyncEvent.initial() = Initial;
const factory DatabaseSyncEvent.syncStateChanged(
DatabaseSyncStatePB syncState,
) = syncStateChanged;
const factory DatabaseSyncEvent.networkStateChanged(
ConnectivityResult result,
) = NetworkStateChanged;
}
@freezed
class DatabaseSyncBlocState with _$DatabaseSyncBlocState {
const factory DatabaseSyncBlocState({
required DatabaseSyncState syncState,
@Default(true) bool isNetworkConnected,
@Default(false) bool shouldShowIndicator,
}) = _DatabaseSyncState;
factory DatabaseSyncBlocState.initial() => const DatabaseSyncBlocState(
syncState: DatabaseSyncState.Syncing,
);
}
@@ -0,0 +1,63 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
typedef DatabaseSyncStateCallback = void Function(
DatabaseSyncStatePB syncState,
);
class DatabaseSyncStateListener {
DatabaseSyncStateListener({
// NOTE: NOT the view id.
required this.databaseId,
});
final String databaseId;
StreamSubscription<SubscribeObject>? _subscription;
DatabaseNotificationParser? _parser;
DatabaseSyncStateCallback? didReceiveSyncState;
void start({
DatabaseSyncStateCallback? didReceiveSyncState,
}) {
this.didReceiveSyncState = didReceiveSyncState;
_parser = DatabaseNotificationParser(
id: databaseId,
callback: _callback,
);
_subscription = RustStreamReceiver.listen(
(observable) => _parser?.parse(observable),
);
}
void _callback(
DatabaseNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
switch (ty) {
case DatabaseNotification.DidUpdateDatabaseSyncUpdate:
result.map(
(r) {
final value = DatabaseSyncStatePB.fromBuffer(r);
didReceiveSyncState?.call(value);
},
);
break;
default:
break;
}
}
Future<void> stop() async {
await _subscription?.cancel();
_subscription = null;
}
}
@@ -11,9 +11,11 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import '../../application/database_controller.dart';
@@ -383,21 +385,28 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
groupList.insert(insertGroups.index, group);
add(BoardEvent.didReceiveGroups(groupList));
},
onUpdateGroup: (updatedGroups) {
onUpdateGroup: (updatedGroups) async {
if (isClosed) {
return;
}
// workaround: update group most of the time gets called before fields in
// field controller are updated. For single and multi-select group
// renames, this is required before generating the new group name.
await Future.delayed(const Duration(milliseconds: 50));
for (final group in updatedGroups) {
// see if the column is already in the board
final index = groupList.indexWhere((g) => g.groupId == group.groupId);
if (index == -1) continue;
if (index == -1) {
continue;
}
final columnController =
boardController.getGroupController(group.groupId);
if (columnController != null) {
// remove the group or update its name
columnController.updateGroupName(group.groupName);
columnController.updateGroupName(generateGroupNameFromGroup(group));
if (!group.isVisible) {
boardController.removeGroup(group.groupId);
}
@@ -491,7 +500,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
AppFlowyGroupData _initializeGroupData(GroupPB group) {
return AppFlowyGroupData(
id: group.groupId,
name: group.groupName,
name: generateGroupNameFromGroup(group),
items: _buildGroupItems(group),
customData: GroupData(
group: group,
@@ -499,6 +508,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
),
);
}
String generateGroupNameFromGroup(GroupPB group) {
final field = fieldController.getField(group.fieldId);
if (field == null) {
return "";
}
// if the group is the default group, then
if (group.isDefault) {
return "No ${field.name}";
}
switch (field.fieldType) {
case FieldType.SingleSelect:
final options =
SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
.options;
final option =
options.firstWhereOrNull((option) => option.id == group.groupId);
return option == null ? "" : option.name;
case FieldType.MultiSelect:
final options =
MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
.options;
final option =
options.firstWhereOrNull((option) => option.id == group.groupId);
return option == null ? "" : option.name;
case FieldType.Checkbox:
return group.groupId;
case FieldType.URL:
return group.groupId;
case FieldType.DateTime:
// Assume DateCondition::Relative as there isn't an option for this
// right now.
final dateFormat = DateFormat("y/MM/dd");
try {
final targetDateTime = dateFormat.parseLoose(group.groupId);
final targetDateTimeDay = DateTime(
targetDateTime.year,
targetDateTime.month,
targetDateTime.day,
);
final now = DateTime.now();
final nowDay = DateTime(
now.year,
now.month,
now.day,
);
final diff = targetDateTimeDay.difference(nowDay).inDays;
return switch (diff) {
0 => "Today",
-1 => "Yesterday",
1 => "Tomorrow",
-7 => "Last 7 days",
2 => "Next 7 days",
-30 => "Last 30 days",
8 => "Next 30 days",
_ => DateFormat("MMM y").format(targetDateTimeDay)
};
} on FormatException {
return "";
}
default:
return "";
}
}
}
@freezed
@@ -31,23 +31,11 @@ class GroupController {
final GroupControllerDelegate delegate;
final void Function(GroupPB group) onGroupChanged;
RowMetaPB? rowAtIndex(int index) {
if (index < group.rows.length) {
return group.rows[index];
} else {
return null;
}
}
RowMetaPB? rowAtIndex(int index) => group.rows.elementAtOrNull(index);
RowMetaPB? firstRow() {
if (group.rows.isEmpty) return null;
return group.rows.first;
}
RowMetaPB? firstRow() => group.rows.firstOrNull;
RowMetaPB? lastRow() {
if (group.rows.isEmpty) return null;
return group.rows.last;
}
RowMetaPB? lastRow() => group.rows.lastOrNull;
void startListening() {
_listener.start(
@@ -1,5 +1,7 @@
import 'dart:collection';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
@@ -34,6 +36,8 @@ import 'toolbar/board_setting_bar.dart';
import 'widgets/board_hidden_groups.dart';
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override
Widget content(
BuildContext context,
@@ -49,14 +53,27 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
BoardSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
toggleExtension: _toggleExtension,
);
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) =>
const SizedBox.shrink();
) {
return DatabaseViewSettingExtension(
key: _makeValueKey(controller),
viewId: controller.viewId,
databaseController: controller,
toggleExtension: _toggleExtension,
);
}
@override
void dispose() {
_toggleExtension.dispose();
super.dispose();
}
ValueKey _makeValueKey(DatabaseController controller) =>
ValueKey(controller.viewId);
@@ -1,24 +1,53 @@
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardSettingBar extends StatelessWidget {
const BoardSettingBar({
super.key,
required this.databaseController,
required this.toggleExtension,
});
final DatabaseController databaseController;
final ToggleExtensionNotifier toggleExtension;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SettingButton(databaseController: databaseController),
],
return BlocProvider<DatabaseFilterMenuBloc>(
create: (context) => DatabaseFilterMenuBloc(
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
)..add(const DatabaseFilterMenuEvent.initial()),
child: BlocListener<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
child: ValueListenableBuilder<bool>(
valueListenable: databaseController.isLoading,
builder: (context, value, child) {
if (value) {
return const SizedBox.shrink();
}
return SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const FilterButton(),
const HSpace(2),
SettingButton(
databaseController: databaseController,
),
],
),
);
},
),
),
);
}
@@ -269,7 +269,7 @@ class HiddenGroupButtonContent extends StatelessWidget {
),
const HSpace(4),
FlowyText.medium(
group.groupName,
bloc.generateGroupNameFromGroup(group),
overflow: TextOverflow.ellipsis,
),
const HSpace(6),
@@ -369,7 +369,7 @@ class HiddenGroupPopupItemList extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
group.groupName,
context.read<BoardBloc>().generateGroupNameFromGroup(group),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
@@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
class DatabaseBackendService {
static Future<FlowyResult<List<DatabaseDescriptionPB>, FlowyError>>
static Future<FlowyResult<List<DatabaseMetaPB>, FlowyError>>
getAllDatabases() {
return DatabaseEventGetDatabases().send().then((result) {
return result.fold(
@@ -62,6 +62,19 @@ class FieldBackendService {
return DatabaseEventDeleteField(payload).send();
}
// Clear all data of all cells in a Field
static Future<FlowyResult<void, FlowyError>> clearField({
required String viewId,
required String fieldId,
}) {
final payload = ClearFieldPayloadPB(
viewId: viewId,
fieldId: fieldId,
);
return DatabaseEventClearField(payload).send();
}
/// Duplicate a field
static Future<FlowyResult<void, FlowyError>> duplicateField({
required String viewId,
@@ -1,15 +1,6 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart' as $fixnum;
@@ -109,25 +100,21 @@ class FilterBackendService {
int? timestamp,
}) {
assert(
[
FieldType.DateTime,
FieldType.LastEditedTime,
FieldType.CreatedTime,
].contains(fieldType),
fieldType == FieldType.DateTime ||
fieldType == FieldType.LastEditedTime ||
fieldType == FieldType.CreatedTime,
);
final filter = DateFilterPB();
if (timestamp != null) {
filter.timestamp = $fixnum.Int64(timestamp);
} else {
if (start != null && end != null) {
filter.start = $fixnum.Int64(start);
filter.end = $fixnum.Int64(end);
} else {
throw Exception(
"Start and end should not be null if the timestamp is null",
);
}
}
if (start != null) {
filter.start = $fixnum.Int64(start);
}
if (end != null) {
filter.end = $fixnum.Int64(end);
}
return filterId == null
@@ -2,8 +2,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'type_option_service.dart';
import 'package:nanoid/nanoid.dart';
class SelectOptionCellBackendService {
SelectOptionCellBackendService({
@@ -18,26 +17,23 @@ class SelectOptionCellBackendService {
Future<FlowyResult<void, FlowyError>> create({
required String name,
SelectOptionColorPB? color,
bool isSelected = true,
}) {
return TypeOptionBackendService(viewId: viewId, fieldId: fieldId)
.newOption(name: name)
.then(
(result) {
return result.fold(
(option) {
final payload = RepeatedSelectOptionPayload()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..items.add(option);
final option = SelectOptionPB()
..id = nanoid(4)
..name = name;
if (color != null) {
option.color = color;
}
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
},
(r) => FlowyResult.failure(r),
);
},
);
final payload = RepeatedSelectOptionPayload()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..items.add(option);
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
}
Future<FlowyResult<void, FlowyError>> update({
@@ -8,11 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'filter_menu_bloc.freezed.dart';
class GridFilterMenuBloc
extends Bloc<GridFilterMenuEvent, GridFilterMenuState> {
GridFilterMenuBloc({required this.viewId, required this.fieldController})
class DatabaseFilterMenuBloc
extends Bloc<DatabaseFilterMenuEvent, DatabaseFilterMenuState> {
DatabaseFilterMenuBloc({required this.viewId, required this.fieldController})
: super(
GridFilterMenuState.initial(
DatabaseFilterMenuState.initial(
viewId,
fieldController.filterInfos,
fieldController.fieldInfos,
@@ -27,7 +27,7 @@ class GridFilterMenuBloc
void Function(List<FieldInfo>)? _onFieldFn;
void _dispatch() {
on<GridFilterMenuEvent>(
on<DatabaseFilterMenuEvent>(
(event, emit) async {
event.when(
initial: () {
@@ -55,11 +55,11 @@ class GridFilterMenuBloc
void _startListening() {
_onFilterFn = (filters) {
add(GridFilterMenuEvent.didReceiveFilters(filters));
add(DatabaseFilterMenuEvent.didReceiveFilters(filters));
};
_onFieldFn = (fields) {
add(GridFilterMenuEvent.didReceiveFields(fields));
add(DatabaseFilterMenuEvent.didReceiveFields(fields));
};
fieldController.addListener(
@@ -87,32 +87,33 @@ class GridFilterMenuBloc
}
@freezed
class GridFilterMenuEvent with _$GridFilterMenuEvent {
const factory GridFilterMenuEvent.initial() = _Initial;
const factory GridFilterMenuEvent.didReceiveFilters(
class DatabaseFilterMenuEvent with _$DatabaseFilterMenuEvent {
const factory DatabaseFilterMenuEvent.initial() = _Initial;
const factory DatabaseFilterMenuEvent.didReceiveFilters(
List<FilterInfo> filters,
) = _DidReceiveFilters;
const factory GridFilterMenuEvent.didReceiveFields(List<FieldInfo> fields) =
_DidReceiveFields;
const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
const factory DatabaseFilterMenuEvent.didReceiveFields(
List<FieldInfo> fields,
) = _DidReceiveFields;
const factory DatabaseFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
}
@freezed
class GridFilterMenuState with _$GridFilterMenuState {
const factory GridFilterMenuState({
class DatabaseFilterMenuState with _$DatabaseFilterMenuState {
const factory DatabaseFilterMenuState({
required String viewId,
required List<FilterInfo> filters,
required List<FieldInfo> fields,
required List<FieldInfo> creatableFields,
required bool isVisible,
}) = _GridFilterMenuState;
}) = _DatabaseFilterMenuState;
factory GridFilterMenuState.initial(
factory DatabaseFilterMenuState.initial(
String viewId,
List<FilterInfo> filterInfos,
List<FieldInfo> fields,
) =>
GridFilterMenuState(
DatabaseFilterMenuState(
viewId: viewId,
filters: filterInfos,
fields: fields,
@@ -36,7 +36,7 @@ class SelectOptionFilterListBloc<T>
emit: emit,
);
},
unselectOption: (option) {
unSelectOption: (option) {
final selectedOptionIds = Set<String>.from(state.selectedOptionIds);
selectedOptionIds.remove(option.id);
@@ -121,7 +121,7 @@ class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent {
SelectOptionPB option,
SelectOptionFilterConditionPB condition,
) = _SelectOption;
const factory SelectOptionFilterListEvent.unselectOption(
const factory SelectOptionFilterListEvent.unSelectOption(
SelectOptionPB option,
) = _UnSelectOption;
const factory SelectOptionFilterListEvent.didReceiveOptions(
@@ -3,8 +3,7 @@ import 'dart:async';
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -12,7 +11,7 @@ part 'text_filter_editor_bloc.freezed.dart';
class TextFilterEditorBloc
extends Bloc<TextFilterEditorEvent, TextFilterEditorState> {
TextFilterEditorBloc({required this.filterInfo})
TextFilterEditorBloc({required this.filterInfo, required this.fieldType})
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
_listener = FilterListener(
viewId: filterInfo.viewId,
@@ -23,6 +22,7 @@ class TextFilterEditorBloc
}
final FilterInfo filterInfo;
final FieldType fieldType;
final FilterBackendService _filterBackendSvc;
final FilterListener _listener;
@@ -34,20 +34,34 @@ class TextFilterEditorBloc
_startListening();
},
updateCondition: (TextFilterConditionPB condition) {
_filterBackendSvc.insertTextFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: condition,
content: state.filter.content,
);
fieldType == FieldType.RichText
? _filterBackendSvc.insertTextFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: condition,
content: state.filter.content,
)
: _filterBackendSvc.insertURLFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: condition,
content: state.filter.content,
);
},
updateContent: (content) {
_filterBackendSvc.insertTextFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: state.filter.condition,
content: content,
);
updateContent: (String content) {
fieldType == FieldType.RichText
? _filterBackendSvc.insertTextFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: state.filter.condition,
content: content,
)
: _filterBackendSvc.insertURLFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: state.filter.condition,
content: content,
);
},
delete: () {
_filterBackendSvc.deleteFilter(
@@ -96,9 +96,9 @@ extension SelectOptionFilterConditionPBExtension
SelectOptionFilterConditionPB.OptionIsNot =>
LocaleKeys.grid_selectOptionFilter_isNot.tr(),
SelectOptionFilterConditionPB.OptionContains =>
LocaleKeys.grid_selectOptionFilter_isNot.tr(),
LocaleKeys.grid_selectOptionFilter_contains.tr(),
SelectOptionFilterConditionPB.OptionDoesNotContain =>
LocaleKeys.grid_selectOptionFilter_isNot.tr(),
LocaleKeys.grid_selectOptionFilter_doesNotContain.tr(),
SelectOptionFilterConditionPB.OptionIsEmpty =>
LocaleKeys.grid_selectOptionFilter_isEmpty.tr(),
SelectOptionFilterConditionPB.OptionIsNotEmpty =>
@@ -3,7 +3,7 @@ import 'package:appflowy/plugins/database/grid/application/filter/select_option_
import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -89,7 +89,7 @@ class _SelectOptionFilterCellState extends State<SelectOptionFilterCell> {
if (widget.isSelected) {
context
.read<SelectOptionFilterListBloc>()
.add(SelectOptionFilterListEvent.unselectOption(widget.option));
.add(SelectOptionFilterListEvent.unSelectOption(widget.option));
} else {
context.read<SelectOptionFilterListBloc>().add(
SelectOptionFilterListEvent.selectOption(
@@ -32,9 +32,22 @@ class SingleSelectOptionFilterDelegateImpl
) {
final selectOptionIds = Set<String>.from(currentOptionIds);
if (condition == SelectOptionFilterConditionPB.OptionIsNot ||
selectOptionIds.isEmpty) {
selectOptionIds.add(optionId);
switch (condition) {
case SelectOptionFilterConditionPB.OptionIs:
if (selectOptionIds.isNotEmpty) {
selectOptionIds.clear();
}
selectOptionIds.add(optionId);
break;
case SelectOptionFilterConditionPB.OptionIsNot:
selectOptionIds.add(optionId);
break;
case SelectOptionFilterConditionPB.OptionIsEmpty ||
SelectOptionFilterConditionPB.OptionIsNotEmpty:
selectOptionIds.clear();
break;
default:
throw UnimplementedError();
}
return selectOptionIds;
@@ -1,59 +1,44 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/filter/text_filter_editor_bloc.dart';
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class TextFilterChoicechip extends StatefulWidget {
class TextFilterChoicechip extends StatelessWidget {
const TextFilterChoicechip({required this.filterInfo, super.key});
final FilterInfo filterInfo;
@override
State<TextFilterChoicechip> createState() => _TextFilterChoicechipState();
}
class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
late TextFilterEditorBloc bloc;
@override
void initState() {
bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo)
..add(const TextFilterEditorEvent.initial());
super.initState();
}
@override
void dispose() {
bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
return BlocProvider(
create: (_) => TextFilterEditorBloc(
filterInfo: filterInfo,
fieldType: FieldType.RichText,
)..add(const TextFilterEditorEvent.initial()),
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (blocContext, state) {
builder: (context, state) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(200, 76)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (BuildContext context) {
return TextFilterEditor(bloc: bloc);
popupBuilder: (popoverContext) {
return BlocProvider.value(
value: context.read<TextFilterEditorBloc>(),
child: const TextFilterEditor(),
);
},
child: ChoiceChipButton(
filterInfo: widget.filterInfo,
filterInfo: filterInfo,
filterDesc: _makeFilterDesc(state),
),
);
@@ -78,9 +63,7 @@ class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
}
class TextFilterEditor extends StatefulWidget {
const TextFilterEditor({required this.bloc, super.key});
final TextFilterEditorBloc bloc;
const TextFilterEditor({super.key});
@override
State<TextFilterEditor> createState() => _TextFilterEditorState();
@@ -91,26 +74,23 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: widget.bloc,
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPanel(context, state),
];
return BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPanel(context, state),
];
if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) {
children.add(const VSpace(4));
children.add(_buildFilterTextField(context, state));
}
if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) {
children.add(const VSpace(4));
children.add(_buildFilterTextField(context, state));
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: IntrinsicHeight(child: Column(children: children)),
);
},
),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: IntrinsicHeight(child: Column(children: children)),
);
},
);
}
@@ -1,14 +1,58 @@
import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class URLFilterChoicechip extends StatelessWidget {
const URLFilterChoicechip({required this.filterInfo, super.key});
class URLFilterChoiceChip extends StatelessWidget {
const URLFilterChoiceChip({required this.filterInfo, super.key});
final FilterInfo filterInfo;
@override
Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo);
return BlocProvider(
create: (_) => TextFilterEditorBloc(
filterInfo: filterInfo,
fieldType: FieldType.URL,
),
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (context, state) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(200, 76)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (popoverContext) {
return BlocProvider.value(
value: context.read<TextFilterEditorBloc>(),
child: const TextFilterEditor(),
);
},
child: ChoiceChipButton(
filterInfo: filterInfo,
filterDesc: _makeFilterDesc(state),
),
);
},
),
);
}
String _makeFilterDesc(TextFilterEditorState state) {
String filterDesc = state.filter.condition.choicechipPrefix;
if (state.filter.condition == TextFilterConditionPB.TextIsEmpty ||
state.filter.condition == TextFilterConditionPB.TextIsNotEmpty) {
return filterDesc;
}
if (state.filter.content.isNotEmpty) {
filterDesc += " ${state.filter.content}";
}
return filterDesc;
}
}
@@ -21,13 +21,17 @@ class FilterInfo {
String get fieldId => filter.data.fieldId;
DateFilterPB? dateFilter() {
return filter.data.fieldType == FieldType.DateTime
final fieldType = filter.data.fieldType;
return fieldType == FieldType.DateTime ||
fieldType == FieldType.CreatedTime ||
fieldType == FieldType.LastEditedTime
? DateFilterPB.fromBuffer(filter.data.data)
: null;
}
TextFilterPB? textFilter() {
return filter.data.fieldType == FieldType.RichText
return filter.data.fieldType == FieldType.RichText ||
filter.data.fieldType == FieldType.URL
? TextFilterPB.fromBuffer(filter.data.data)
: null;
}
@@ -23,14 +23,14 @@ class FilterMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
return BlocProvider<DatabaseFilterMenuBloc>(
create: (context) => DatabaseFilterMenuBloc(
viewId: fieldController.viewId,
fieldController: fieldController,
)..add(
const GridFilterMenuEvent.initial(),
const DatabaseFilterMenuEvent.initial(),
),
child: BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
child: BlocBuilder<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
builder: (context, state) {
final List<Widget> children = [];
children.addAll(
@@ -115,7 +115,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
triggerActions: PopoverTriggerFlags.none,
child: child,
popupBuilder: (BuildContext context) {
final bloc = buildContext.read<GridFilterMenuBloc>();
final bloc = buildContext.read<DatabaseFilterMenuBloc>();
return GridCreateFilterList(
viewId: widget.viewId,
fieldController: bloc.fieldController,
@@ -26,7 +26,7 @@ class FilterMenuItem extends StatelessWidget {
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
FieldType.SingleSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.URL => URLFilterChoicechip(filterInfo: filterInfo),
FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
_ => const SizedBox(),
};
Loaded 100 of 876 files, more files were not shown because too many files have changed in this diff. Show more