Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd629aaef | ||
|
|
d302f6b3fb | ||
|
|
5595649f53 | ||
|
|
7de17ab0b8 | ||
|
|
13ff38756f | ||
|
|
197e5b3a79 | ||
|
|
5cced92646 | ||
|
|
e50d708c21 | ||
|
|
79fc7c4cfe | ||
|
|
202cd90711 | ||
|
|
177f7c4fa3 | ||
|
|
d986e01d3e | ||
|
|
8a8915c662 | ||
|
|
efc857d752 | ||
|
|
14dee6b797 | ||
|
|
63eee8b415 | ||
|
|
d96a1d8bd4 |
No files matched your search
@@ -102,6 +102,7 @@ jobs:
|
||||
git config --system core.longpaths true
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
dart pub global activate protoc_plugin 20.0.1
|
||||
shell: bash
|
||||
|
||||
- name: Build AppFlowy
|
||||
@@ -119,9 +120,16 @@ jobs:
|
||||
run: |
|
||||
cargo make dart_unit_test
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
- name: Upload coverage to Codecov
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
with:
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_unit_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
action: codecov/codecov-action@v3
|
||||
with: |
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_unit_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
attempt_limit: 5
|
||||
attempt_delay: 10000
|
||||
|
||||
@@ -21,6 +21,8 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -36,31 +38,36 @@ jobs:
|
||||
with:
|
||||
toolchain: "stable-2022-04-07"
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.10.1"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v2
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
prefix-key: ${{ matrix.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
|
||||
- name: Cache Rust
|
||||
id: cache-rust-target
|
||||
uses: actions/cache@v2
|
||||
- uses: davidB/rust-cargo-make@v1
|
||||
with:
|
||||
path: |
|
||||
frontend/rust-lib/target
|
||||
shared-lib/target
|
||||
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
version: '0.36.6'
|
||||
|
||||
- name: Setup Environment
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
@@ -73,14 +80,8 @@ jobs:
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- if: steps.cache-cargo.outputs.cache-hit != 'true'
|
||||
name: Rust Deps
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
|
||||
- name: Config Flutter
|
||||
run: |
|
||||
@@ -91,6 +92,7 @@ jobs:
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
dart pub global activate protoc_plugin 20.0.1
|
||||
shell: bash
|
||||
|
||||
- name: Build Test lib
|
||||
@@ -120,9 +122,15 @@ jobs:
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
- name: Upload coverage to Codecov
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
with:
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_integrateion_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
action: codecov/codecov-action@v3
|
||||
with: |
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_integrateion_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
attempt_limit: 5
|
||||
attempt_delay: 10000
|
||||
@@ -1,4 +1,7 @@
|
||||
# Release Notes
|
||||
## Version 0.2.3 - 06/21/2023
|
||||
### New Features
|
||||
- Added support for creating multiple database views for existing database
|
||||
|
||||
## Version 0.2.2 - 06/15/2023
|
||||
### New Features
|
||||
|
||||
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.2.2"
|
||||
CURRENT_APP_VERSION = "0.2.3"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -72,3 +72,5 @@ windows/flutter/dart_ffi/
|
||||
*.env
|
||||
|
||||
coverage/
|
||||
|
||||
**/failures/*.png
|
||||
@@ -0,0 +1,11 @@
|
||||
"{""id"":""2_OVWb"",""name"":""Name"",""field_type"":0,""visibility"":true,""width"":150,""type_options"":{""0"":{""data"":""""}},""is_primary"":true}","{""id"":""xjmOSi"",""name"":""Type"",""field_type"":3,""visibility"":true,""width"":150,""type_options"":{""3"":{""content"":""{\""options\"":[{\""id\"":\""t1WZ\"",\""name\"":\""s6\"",\""color\"":\""Lime\""},{\""id\"":\""GzNa\"",\""name\"":\""s5\"",\""color\"":\""Yellow\""},{\""id\"":\""l_8w\"",\""name\"":\""s4\"",\""color\"":\""Orange\""},{\""id\"":\""TzVT\"",\""name\"":\""s3\"",\""color\"":\""LightPink\""},{\""id\"":\""b5WF\"",\""name\"":\""s2\"",\""color\"":\""Pink\""},{\""id\"":\""AcHA\"",\""name\"":\""s1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""Hpbiwr"",""name"":""Done"",""field_type"":5,""visibility"":true,""width"":150,""type_options"":{""5"":{""is_selected"":false}},""is_primary"":false}","{""id"":""F7WLnw"",""name"":""checklist"",""field_type"":7,""visibility"":true,""width"":120,""type_options"":{""0"":{""data"":""""},""7"":{}},""is_primary"":false}","{""id"":""KABhMe"",""name"":""number"",""field_type"":1,""visibility"":true,""width"":120,""type_options"":{""1"":{""format"":0,""symbol"":""RUB"",""scale"":0,""name"":""Number""},""0"":{""scale"":0,""data"":"""",""format"":0,""name"":""Number"",""symbol"":""RUB""}},""is_primary"":false}","{""id"":""lEn6Bv"",""name"":""date"",""field_type"":2,""visibility"":true,""width"":120,""type_options"":{""2"":{""field_type"":2,""time_format"":1,""timezone_id"":"""",""date_format"":3},""0"":{""field_type"":2,""date_format"":3,""time_format"":1,""data"":"""",""timezone_id"":""""}},""is_primary"":false}","{""id"":""B8Prnx"",""name"":""url"",""field_type"":6,""visibility"":true,""width"":120,""type_options"":{""6"":{""content"":"""",""url"":""""},""0"":{""content"":"""",""data"":"""",""url"":""""}},""is_primary"":false}","{""id"":""MwUow4"",""name"":""multi-select"",""field_type"":4,""visibility"":true,""width"":240,""type_options"":{""0"":{""content"":""{\""options\"":[],\""disable_color\"":false}"",""data"":""""},""4"":{""content"":""{\""options\"":[{\""id\"":\""__Us\"",\""name\"":\""m7\"",\""color\"":\""Green\""},{\""id\"":\""n9-g\"",\""name\"":\""m6\"",\""color\"":\""Lime\""},{\""id\"":\""KFYu\"",\""name\"":\""m5\"",\""color\"":\""Yellow\""},{\""id\"":\""KftP\"",\""name\"":\""m4\"",\""color\"":\""Orange\""},{\""id\"":\""5lWo\"",\""name\"":\""m3\"",\""color\"":\""LightPink\""},{\""id\"":\""Djrz\"",\""name\"":\""m2\"",\""color\"":\""Pink\""},{\""id\"":\""2uRu\"",\""name\"":\""m1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}"
|
||||
"{""field_type"":0,""created_at"":1686793246,""data"":""A"",""last_modified"":1686793246}","{""last_modified"":1686793275,""created_at"":1686793261,""data"":""AcHA"",""field_type"":3}","{""created_at"":1686793241,""field_type"":5,""last_modified"":1686793241,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""pi1A\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""6Pym\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""erEe\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""pi1A\"",\""6Pym\""]}"",""created_at"":1686793302,""field_type"":7,""last_modified"":1686793308}","{""created_at"":1686793333,""field_type"":1,""data"":""-1"",""last_modified"":1686793333}","{""last_modified"":1686793370,""field_type"":2,""data"":""1685583770"",""include_time"":false,""created_at"":1686793370}","{""created_at"":1686793395,""data"":""appflowy.io"",""field_type"":6,""last_modified"":1686793399,""url"":""https://appflowy.io""}","{""last_modified"":1686793446,""field_type"":4,""data"":""2uRu"",""created_at"":1686793428}"
|
||||
"{""last_modified"":1686793247,""data"":""B"",""field_type"":0,""created_at"":1686793247}","{""created_at"":1686793278,""data"":""b5WF"",""field_type"":3,""last_modified"":1686793278}","{""created_at"":1686793292,""last_modified"":1686793292,""data"":""Yes"",""field_type"":5}","{""data"":""{\""options\"":[{\""id\"":\""YHDO\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""QjtW\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""K2nM\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""YHDO\""]}"",""field_type"":7,""last_modified"":1686793318,""created_at"":1686793311}","{""data"":""-2"",""last_modified"":1686793335,""created_at"":1686793335,""field_type"":1}","{""field_type"":2,""data"":""1685670174"",""include_time"":false,""created_at"":1686793374,""last_modified"":1686793374}","{""last_modified"":1686793403,""field_type"":6,""created_at"":1686793399,""url"":"""",""data"":""no url""}","{""data"":""2uRu,Djrz"",""field_type"":4,""last_modified"":1686793449,""created_at"":1686793449}"
|
||||
"{""data"":""C"",""created_at"":1686793248,""last_modified"":1686793248,""field_type"":0}","{""created_at"":1686793280,""field_type"":3,""data"":""TzVT"",""last_modified"":1686793280}","{""data"":""Yes"",""last_modified"":1686793292,""field_type"":5,""created_at"":1686793292}","{""last_modified"":1686793329,""field_type"":7,""created_at"":1686793322,""data"":""{\""options\"":[{\""id\"":\""iWM1\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""WDvF\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""w3k7\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""iWM1\"",\""WDvF\"",\""w3k7\""]}""}","{""field_type"":1,""last_modified"":1686793339,""data"":""0.1"",""created_at"":1686793339}","{""last_modified"":1686793377,""data"":""1685756577"",""created_at"":1686793377,""include_time"":false,""field_type"":2}","{""created_at"":1686793403,""field_type"":6,""data"":""appflowy.io"",""last_modified"":1686793408,""url"":""https://appflowy.io""}","{""data"":""2uRu,Djrz,5lWo"",""created_at"":1686793453,""last_modified"":1686793454,""field_type"":4}"
|
||||
"{""data"":""D"",""last_modified"":1686793249,""created_at"":1686793249,""field_type"":0}","{""data"":""l_8w"",""created_at"":1686793284,""last_modified"":1686793284,""field_type"":3}","{""data"":""Yes"",""created_at"":1686793293,""last_modified"":1686793293,""field_type"":5}",,"{""field_type"":1,""last_modified"":1686793341,""created_at"":1686793341,""data"":""0.2""}","{""created_at"":1686793379,""last_modified"":1686793379,""field_type"":2,""data"":""1685842979"",""include_time"":false}","{""last_modified"":1686793419,""field_type"":6,""created_at"":1686793408,""data"":""https://github.com/AppFlowy-IO/"",""url"":""https://github.com/AppFlowy-IO/""}","{""data"":""2uRu,Djrz,5lWo"",""last_modified"":1686793459,""field_type"":4,""created_at"":1686793459}"
|
||||
"{""field_type"":0,""last_modified"":1686793250,""created_at"":1686793250,""data"":""E""}","{""field_type"":3,""last_modified"":1686793290,""created_at"":1686793290,""data"":""GzNa""}","{""last_modified"":1686793294,""created_at"":1686793294,""data"":""Yes"",""field_type"":5}",,"{""created_at"":1686793346,""field_type"":1,""last_modified"":1686793346,""data"":""1""}","{""last_modified"":1686793383,""data"":""1685929383"",""field_type"":2,""include_time"":false,""created_at"":1686793383}","{""field_type"":6,""url"":"""",""data"":"""",""last_modified"":1686793421,""created_at"":1686793419}","{""field_type"":4,""last_modified"":1686793465,""data"":""2uRu,Djrz,5lWo,KFYu,KftP"",""created_at"":1686793463}"
|
||||
"{""field_type"":0,""created_at"":1686793251,""data"":"""",""last_modified"":1686793289}",,,,"{""data"":""2"",""field_type"":1,""created_at"":1686793347,""last_modified"":1686793347}","{""include_time"":false,""data"":""1685929385"",""last_modified"":1686793385,""field_type"":2,""created_at"":1686793385}",,
|
||||
"{""created_at"":1686793254,""field_type"":0,""last_modified"":1686793288,""data"":""""}",,,,"{""created_at"":1686793351,""last_modified"":1686793351,""data"":""10"",""field_type"":1}","{""include_time"":false,""data"":""1686879792"",""field_type"":2,""created_at"":1686793392,""last_modified"":1686793392}",,
|
||||
,,,,"{""last_modified"":1686793354,""created_at"":1686793354,""field_type"":1,""data"":""11""}",,,
|
||||
,,,,"{""field_type"":1,""last_modified"":1686793356,""data"":""12"",""created_at"":1686793356}",,,
|
||||
,,,,,,,
|
||||
@@ -55,7 +55,8 @@
|
||||
"textAndMarkdown": "Text & Markdown",
|
||||
"documentFromV010": "Document from v0.1.0",
|
||||
"databaseFromV010": "Database from v0.1.0",
|
||||
"csv": "CSV"
|
||||
"csv": "CSV",
|
||||
"database": "Database"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Rename",
|
||||
@@ -222,6 +223,7 @@
|
||||
"changeLocationTooltips": "Change the data directory",
|
||||
"change": "Change",
|
||||
"openLocationTooltips": "Open another data directory",
|
||||
"openCurrentDataFolder": "Open current data directory",
|
||||
"recoverLocationTooltips": "Reset to AppFlowy's default data directory",
|
||||
"exportFileSuccess": "Export file successfully!",
|
||||
"exportFileFail": "Export file failed!",
|
||||
@@ -235,6 +237,8 @@
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"deleteView": "Are you sure you want to delete this view?",
|
||||
"createView": "New",
|
||||
"settings": {
|
||||
"filter": "Filter",
|
||||
"sort": "Sort",
|
||||
@@ -474,5 +478,10 @@
|
||||
"name": "Calendar layout"
|
||||
},
|
||||
"referencedCalendarPrefix": "View of"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "AppFlowy Error",
|
||||
"howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.",
|
||||
"github": "View on GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,14 @@
|
||||
"medium": "средний",
|
||||
"large": "большой",
|
||||
"fontSize": "Размер шрифта",
|
||||
"import": "Импортировать"
|
||||
"import": "Импортировать",
|
||||
"moreOptions": ""
|
||||
},
|
||||
"importPanel": {
|
||||
"textAndMarkdown": "Текст и Markdown",
|
||||
"documentFromV010": "Документ из v0.1.0",
|
||||
"databaseFromV010": "База данных из v0.1.0",
|
||||
"csv": "CSV"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Переименовать",
|
||||
@@ -108,8 +115,9 @@
|
||||
"openAsPage": "Открыть как страницу",
|
||||
"addNewRow": "Добавить новую строку",
|
||||
"openMenu": "Открыть меню",
|
||||
"viewDataBase": "Просмотр базы данных",
|
||||
"referencePage": "Ссылаются на {name}"
|
||||
"dragRow": "Долгое нажатие для изменения порядка строк",
|
||||
"viewDataBase": "Открыть базу данных",
|
||||
"referencePage": "Ссылки на {name}"
|
||||
},
|
||||
"sideBar": {
|
||||
"closeSidebar": "Закрыть боковое меню",
|
||||
@@ -123,7 +131,7 @@
|
||||
},
|
||||
"contactsPage": {
|
||||
"title": "Контакты",
|
||||
"whatsHappening": "Какие события на этой неделе?",
|
||||
"whatsHappening": "Что нового на этой неделе?",
|
||||
"addContact": "Добавить контакт",
|
||||
"editContact": "Редактировать контакт"
|
||||
},
|
||||
@@ -182,15 +190,20 @@
|
||||
"theme": "Тема"
|
||||
},
|
||||
"files": {
|
||||
"copy": "Копировать",
|
||||
"defaultLocation": "Где сейчас хранятся ваши данные",
|
||||
"exportData": "Экспорт данных",
|
||||
"doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь",
|
||||
"restoreLocation": "Восстановить путь AppFlowy по умолчанию",
|
||||
"customizeLocation": "Открыть другую папку",
|
||||
"restartApp": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.",
|
||||
"exportDatabase": "Экспорт базы данных",
|
||||
"selectFiles": "Выбрать файлы, которые необходимо экспортировать",
|
||||
"selectAll": "Выбрать всё",
|
||||
"deselectAll": "Снять выделение",
|
||||
"createNewFolder": "Создать новую папку",
|
||||
"createNewFolderDesc": "Указать, где хранить свои данные ...",
|
||||
"defineWhereYourDataIsStored": "Указать хранилище данных",
|
||||
"open": "Открыть",
|
||||
"openFolder": "Открыть существующую папку",
|
||||
"openFolderDesc": "Чтение и запись в существующую папку AppFlowy ...",
|
||||
@@ -199,9 +212,17 @@
|
||||
"locationDesc": "Выбрать имя папки данных AppFlowy",
|
||||
"browser": "Обзор",
|
||||
"create": "Создать",
|
||||
"set": "Set",
|
||||
"folderPath": "Путь к вашей папке",
|
||||
"locationCannotBeEmpty": "Путь не может быть пустым",
|
||||
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!"
|
||||
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!",
|
||||
"changeLocationTooltips": "Сменить местоположения",
|
||||
"change": "Изменить",
|
||||
"openLocationTooltips": "Открыть другую директорию данных",
|
||||
"recoverLocationTooltips": "Сбросить к местоположению по умолчанию",
|
||||
"exportFileSuccess": "Экспорт завершён!",
|
||||
"exportFileFail": "Не удалость экспортировать!",
|
||||
"export": "Экспорт"
|
||||
},
|
||||
"user": {
|
||||
"name": "Имя",
|
||||
@@ -221,7 +242,8 @@
|
||||
"deleteFilter": "Удалить фильтр",
|
||||
"filterBy": "Фильтровать по...",
|
||||
"typeAValue": "Введите значение...",
|
||||
"layout": "Раскладка"
|
||||
"layout": "Вид",
|
||||
"databaseLayout": "Вид базы данных"
|
||||
},
|
||||
"textFilter": {
|
||||
"contains": "Содержит",
|
||||
@@ -272,6 +294,8 @@
|
||||
"textFieldName": "Текст",
|
||||
"checkboxFieldName": "Чекбокс",
|
||||
"dateFieldName": "Дата",
|
||||
"updatedAtFieldName": "Последнее изменение",
|
||||
"createdAtFieldName": "Создано",
|
||||
"numberFieldName": "Число",
|
||||
"singleSelectFieldName": "Выбор",
|
||||
"multiSelectFieldName": "Выбор нескольких",
|
||||
@@ -341,12 +365,16 @@
|
||||
},
|
||||
"slashMenu": {
|
||||
"board": {
|
||||
"selectABoardToLinkTo": "Выбрать доску для связи с",
|
||||
"selectABoardToLinkTo": "Выбрать доску",
|
||||
"createANewBoard": "Создать доску"
|
||||
},
|
||||
"grid": {
|
||||
"selectAGridToLinkTo": "Выберите сетку для связи с",
|
||||
"selectAGridToLinkTo": "Выберите сетку",
|
||||
"createANewGrid": "Создать сетку"
|
||||
},
|
||||
"calendar": {
|
||||
"selectACalendarToLinkTo": "Выбрать календарь",
|
||||
"createANewCalendar": "Создать календарь"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
@@ -363,6 +391,8 @@
|
||||
"smartEditFixSpelling": "Исправить правописание",
|
||||
"warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.",
|
||||
"smartEditSummarize": "Выделить суть",
|
||||
"smartEditImproveWriting": "Улучшить",
|
||||
"smartEditMakeLonger": "Продолжить",
|
||||
"smartEditCouldNotFetchResult": "Не могу получить ответ от OpenAI",
|
||||
"smartEditCouldNotFetchKey": "Не могу получить токен OpenAI",
|
||||
"smartEditDisabled": "Подключить OpenAI",
|
||||
@@ -394,6 +424,21 @@
|
||||
"mathEquation": {
|
||||
"addMathEquation": "Добавить математическое выражение",
|
||||
"editMathEquation": "Редактировать математическое выражение"
|
||||
},
|
||||
"optionAction": {
|
||||
"click": "Кликните",
|
||||
"toOpenMenu": " чтобы открыть меню",
|
||||
"delete": "Удалить",
|
||||
"duplicate": "Дублировать",
|
||||
"turnInto": "Превратить",
|
||||
"moveUp": "Поднять",
|
||||
"moveDown": "Опустить",
|
||||
"color": "Цвет",
|
||||
"align": "Выравнивание",
|
||||
"left": "По левому краю",
|
||||
"center": "По центру",
|
||||
"right": "По правому краю",
|
||||
"defaultColor": "Цвет по умолчанию"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -417,7 +462,12 @@
|
||||
"showWeekNumbers": "Показывать номера недель",
|
||||
"showWeekends": "Показывать выходные",
|
||||
"firstDayOfWeek": "Первый день недели",
|
||||
"layoutDateField": "Вид календаря"
|
||||
}
|
||||
"layoutDateField": "Вид календаря",
|
||||
"noDateTitle": "No Date",
|
||||
"noDateHint": "Unscheduled events will show up here",
|
||||
"clickToAdd": "Click to add to the calendar",
|
||||
"name": "Calendar layout"
|
||||
},
|
||||
"referencedCalendarPrefix": "Вид"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('calendar start from day setting', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create calendar view
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar');
|
||||
|
||||
// Open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapCalendarLayoutSettingButton();
|
||||
|
||||
// select the first day of week is Monday
|
||||
await tester.tapFirstDayOfWeek();
|
||||
await tester.tapFirstDayOfWeekStartFromMonday();
|
||||
|
||||
// Open the other page and open the new calendar page again
|
||||
await tester.openPage(readme);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
await tester.openPage('calendar');
|
||||
|
||||
// Open setting again and check the start from Monday is selected
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapCalendarLayoutSettingButton();
|
||||
await tester.tapFirstDayOfWeek();
|
||||
tester.assertFirstDayOfWeekStartFromMonday();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -181,5 +181,30 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit single select cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
const fieldType = FieldType.SingleSelect;
|
||||
await tester.tapAddButton();
|
||||
// When create a grid, it will create a single select field by default
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Tap the cell to invoke the selection option editor
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
await tester.createOption(name: 'hello world');
|
||||
await tester.dismissSelectOptionEditor();
|
||||
|
||||
// Make sure the option is created and displayed in the cell
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'hello world',
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -114,7 +114,6 @@ void main() {
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
await tester.createField(FieldType.RichText, 'New field 1');
|
||||
|
||||
// Delete the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'import_files';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('add text filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name');
|
||||
await tester.tapFilterButtonInGrid('Name');
|
||||
|
||||
// enter 'A' in the filter text field
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
await tester.enterTextInTextFilter('A');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// after remove the filter, the grid should show all rows
|
||||
await tester.enterTextInTextFilter('');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.enterTextInTextFilter('B');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// open the menu to delete the filter
|
||||
await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checkbox filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Done');
|
||||
await tester.tapCheckboxFilterButtonInGrid();
|
||||
|
||||
await tester.tapUnCheckedButtonOnCheckboxFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester
|
||||
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checklist filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');
|
||||
|
||||
// By default, the condition of checklist filter is 'uncompleted'
|
||||
await tester.assertNumberOfRowsInGridPage(9);
|
||||
|
||||
await tester.tapFilterButtonInGrid('checklist');
|
||||
await tester.tapChecklistFilterButtonInGrid();
|
||||
|
||||
await tester.tapCompletedButtonOnChecklistFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add single select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type');
|
||||
|
||||
await tester.tapFilterButtonInGrid('Type');
|
||||
|
||||
// select the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
// unselect the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
// select the option 's5'
|
||||
await tester.tapOptionFilterWithName('s5');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// select the option 's4'
|
||||
await tester.tapOptionFilterWithName('s4');
|
||||
|
||||
// The row with 's4' or 's5' should be shown.
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add multi select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.MultiSelect,
|
||||
'multi-select',
|
||||
);
|
||||
|
||||
await tester.tapFilterButtonInGrid('multi-select');
|
||||
await tester.scrollOptionFilterListByOffset(const Offset(0, -200));
|
||||
|
||||
// select the option 'm1'. Any option with 'm1' should be shown.
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
|
||||
// select the option 'm2'. Any option with 'm2' should be shown.
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
|
||||
// select the option 'm4'. Any option with 'm4' should be shown.
|
||||
await tester.tapOptionFilterWithName('m4');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -28,18 +28,6 @@ void main() {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('open first row of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
});
|
||||
|
||||
testWidgets('insert emoji in the row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -83,7 +71,7 @@ void main() {
|
||||
|
||||
// The emoji already displayed in the row banner
|
||||
final emojiText = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == '😅',
|
||||
(widget) => widget is FlowyText && widget.text == '😅',
|
||||
);
|
||||
|
||||
// The number of emoji should be two. One in the row displayed in the grid
|
||||
@@ -109,7 +97,7 @@ void main() {
|
||||
// Remove the emoji
|
||||
await tester.tapButton(find.byType(RemoveEmojiButton));
|
||||
final emojiText = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == '😀',
|
||||
(widget) => widget is FlowyText && widget.text == '😀',
|
||||
);
|
||||
expect(emojiText, findsNothing);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'import_files';
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('import v0.2.0 database data', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapImportButton();
|
||||
|
||||
final testFileNames = ['v020.afdb'];
|
||||
final fileLocation = await tester.currentFileLocation();
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/database',
|
||||
fileName,
|
||||
),
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: location);
|
||||
await tester.tapDatabaseRawDataButton();
|
||||
await tester.openPage('v020');
|
||||
|
||||
// check the import content
|
||||
// await tester.assertCellContent(
|
||||
// rowIndex: 7,
|
||||
// fieldType: FieldType.RichText,
|
||||
// // fieldName: 'Name',
|
||||
// content: '',
|
||||
// );
|
||||
|
||||
// check the text cell
|
||||
final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];
|
||||
for (final (index, content) in textCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the checkbox cell
|
||||
final checkboxCells = <bool>[
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
];
|
||||
for (final (index, content) in checkboxCells.indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the number cell
|
||||
final numberCells = <String>[
|
||||
'-1',
|
||||
'-2',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
'2',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
''
|
||||
];
|
||||
for (final (index, content) in numberCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the url cell
|
||||
final urlCells = <String>[
|
||||
'appflowy.io',
|
||||
'no url',
|
||||
'appflowy.io',
|
||||
'https://github.com/AppFlowy-IO/',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in urlCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.URL,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the single select cell
|
||||
final singleSelectCells = <String>[
|
||||
's1',
|
||||
's2',
|
||||
's3',
|
||||
's4',
|
||||
's5',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in singleSelectCells.indexed) {
|
||||
await tester.assertSingleSelectOption(
|
||||
rowIndex: index,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the multi select cell
|
||||
final List<List<String>> multiSelectCells = [
|
||||
['m1'],
|
||||
['m1', 'm2'],
|
||||
['m1', 'm2', 'm3'],
|
||||
['m1', 'm2', 'm3'],
|
||||
['m1', 'm2', 'm3', 'm4', 'm5'],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
];
|
||||
for (final (index, contents) in multiSelectCells.indexed) {
|
||||
await tester.assertMultiSelectOption(
|
||||
rowIndex: index,
|
||||
contents: contents,
|
||||
);
|
||||
}
|
||||
|
||||
// check the checklist cell
|
||||
final List<double> checklistCells = [
|
||||
0.6,
|
||||
0.3,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
];
|
||||
for (final (index, percent) in checklistCells.indexed) {
|
||||
await tester.assertChecklistCellInGrid(
|
||||
rowIndex: index,
|
||||
percent: percent,
|
||||
);
|
||||
}
|
||||
|
||||
// check the date cell
|
||||
final List<String> dateCells = [
|
||||
'Jun 01, 2023',
|
||||
'Jun 02, 2023',
|
||||
'Jun 03, 2023',
|
||||
'Jun 04, 2023',
|
||||
'Jun 05, 2023',
|
||||
'Jun 05, 2023',
|
||||
'Jun 16, 2023',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
];
|
||||
for (final (index, content) in dateCells.indexed) {
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.DateTime,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'import_files';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('add text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
// check the text cell order
|
||||
final textCells = <String>[
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
];
|
||||
for (final (index, content) in textCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('Name');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'E',
|
||||
'D',
|
||||
'C',
|
||||
'B',
|
||||
'A',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// delete all sorts
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapAllSortButton();
|
||||
|
||||
// check the text cell order
|
||||
for (final (index, content) in <String>[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checkbox sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
|
||||
// check the checkbox cell order
|
||||
for (final (index, content) in <bool>[
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('Done');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <bool>[
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add number sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
|
||||
// check the number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'',
|
||||
'-2',
|
||||
'-1',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
'2',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('number');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add number and text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
|
||||
// open the sort menu and select number order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('number');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapCreateSortByFieldTypeInSortMenu(
|
||||
FieldType.RichText,
|
||||
'Name',
|
||||
);
|
||||
|
||||
// check number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'',
|
||||
'-1',
|
||||
'-2',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check text cell order
|
||||
for (final (index, content) in <String>[
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('create linked view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// Create grid view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
|
||||
|
||||
// Create calendar view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('rename and delete linked view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// rename board view
|
||||
await tester.renameLinkedView(
|
||||
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
|
||||
'new board',
|
||||
);
|
||||
final findBoard = tester.findTabBarLinkViewByViewName('new board');
|
||||
expect(findBoard, findsOneWidget);
|
||||
|
||||
// delete the board
|
||||
await tester.deleteDatebaseView(findBoard);
|
||||
expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('delete the last database view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// delete the board
|
||||
await tester.deleteDatebaseView(
|
||||
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
+1
-1
@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
+1
-1
@@ -7,7 +7,7 @@ import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/ime.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('edit document', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('redo & undo', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document called Sample
|
||||
const pageName = 'Sample';
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
|
||||
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert 1. to trigger it to be a numbered list
|
||||
await tester.ime.insertText('1. ');
|
||||
expect(find.text('1.', findRichText: true), findsOneWidget);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
|
||||
// undo
|
||||
// numbered list will be reverted to paragraph
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
ParagraphBlockKeys.type,
|
||||
);
|
||||
|
||||
// redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
|
||||
// switch to other page and switch back
|
||||
await tester.openPage(readme);
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// the numbered list should be kept
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('write a readme document', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document called Sample
|
||||
const pageName = 'Sample';
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
|
||||
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// mock inputting the sample
|
||||
final lines = _sample.split('\n');
|
||||
for (final line in lines) {
|
||||
await tester.ime.insertText(line);
|
||||
await tester.ime.insertCharacter('\n');
|
||||
}
|
||||
|
||||
// switch to other page and switch back
|
||||
await tester.openPage(readme);
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// this screenshots are different on different platform, so comment it out temporarily.
|
||||
// check the document
|
||||
// await expectLater(
|
||||
// find.byType(AppFlowyEditor),
|
||||
// matchesGoldenFile('document/edit_document_test.png'),
|
||||
// );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet.
|
||||
const _sample = r'''
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
---
|
||||
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
|
||||
|
||||
[] Type / followed by /bullet or /num to create a list.
|
||||
|
||||
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
|
||||
|
||||
[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
|
||||
---
|
||||
* bulleted list 1
|
||||
|
||||
* bulleted list 2
|
||||
|
||||
* bulleted list 3
|
||||
bulleted list 4
|
||||
---
|
||||
1. numbered list 1
|
||||
|
||||
2. numbered list 2
|
||||
|
||||
3. numbered list 3
|
||||
numbered list 4
|
||||
---
|
||||
" quote''';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -1,11 +1,23 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'switch_folder_test.dart' as switch_folder_test;
|
||||
import 'document_test.dart' as document_test;
|
||||
import 'cover_image_test.dart' as cover_image_test;
|
||||
import 'document/document_test.dart' as document_test;
|
||||
import 'document/cover_image_test.dart' as cover_image_test;
|
||||
import 'share_markdown_test.dart' as share_markdown_test;
|
||||
import 'import_files_test.dart' as import_files_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document/document_with_database_test.dart'
|
||||
as document_with_database_test;
|
||||
import 'document/edit_document_test.dart' as edit_document_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_row_test.dart' as database_row_test;
|
||||
import 'database_setting_test.dart' as database_setting_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_view_test.dart' as database_view_test;
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_sort_test.dart' as database_sort_test;
|
||||
|
||||
/// The main task runner for all integration tests in AppFlowy.
|
||||
///
|
||||
@@ -17,11 +29,27 @@ import 'document_with_database_test.dart' as document_with_database_test;
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
switch_folder_test.main();
|
||||
cover_image_test.main();
|
||||
document_test.main();
|
||||
share_markdown_test.main();
|
||||
import_files_test.main();
|
||||
|
||||
// Document integration tests
|
||||
cover_image_test.main();
|
||||
document_test.main();
|
||||
document_with_database_test.main();
|
||||
edit_document_test.main();
|
||||
|
||||
// Database integration tests
|
||||
database_cell_test.main();
|
||||
database_field_test.main();
|
||||
database_share_test.main();
|
||||
database_row_page_test.main();
|
||||
database_row_test.main();
|
||||
database_setting_test.main();
|
||||
database_filter_test.main();
|
||||
database_sort_test.main();
|
||||
database_view_test.main();
|
||||
database_calendar_test.main();
|
||||
|
||||
// board_test.main();
|
||||
// empty_document_test.main();
|
||||
// smart_menu_test.main();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -25,12 +28,12 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
const String userA = 'userA';
|
||||
const String userB = 'userB';
|
||||
final userA = uuid();
|
||||
final userB = uuid();
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.cleanTestLocation(userB);
|
||||
await TestFolder.setTestLocation(userA);
|
||||
await TestFolder.setTestLocation(p.join(userA, appFlowyDataFolder));
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/main.dart' as app;
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -42,7 +44,7 @@ class TestFolder {
|
||||
|
||||
/// Get default location under development environment.
|
||||
static Future<String> defaultDevelopmentLocation() async {
|
||||
final dir = await appFlowyDocumentDirectory();
|
||||
final dir = await appFlowyApplicationDataDirectory();
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
@@ -60,18 +62,18 @@ class TestFolder {
|
||||
extension AppFlowyTestBase on WidgetTester {
|
||||
Future<void> initializeAppFlowy() async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('hotkey_manager'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
);
|
||||
return;
|
||||
});
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await FlowyRunner.run(FlowyApp(), IntegrationMode.integrationTest);
|
||||
|
||||
await app.main();
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
}
|
||||
@@ -83,7 +85,11 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
bool warnIfMissed = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(finder);
|
||||
await tap(
|
||||
finder,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
}
|
||||
@@ -99,7 +105,7 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
);
|
||||
if (button.evaluate().isEmpty) {
|
||||
button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == tr,
|
||||
(widget) => widget is FlowyText && widget.text == tr,
|
||||
);
|
||||
}
|
||||
await tapButton(
|
||||
@@ -130,7 +136,7 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
extension AppFlowyFinderTestBase on CommonFinders {
|
||||
Finder findTextInFlowyText(String text) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == text,
|
||||
(widget) => widget is FlowyText && widget.text == text,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@@ -47,6 +48,13 @@ extension CommonOperations on WidgetTester {
|
||||
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the create grid button.
|
||||
///
|
||||
/// Must call [tapAddButton] first.
|
||||
Future<void> tapCreateCalendarButton() async {
|
||||
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the import button.
|
||||
///
|
||||
/// Must call [tapAddButton] first.
|
||||
@@ -140,6 +148,13 @@ extension CommonOperations on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
/// open the page with given name.
|
||||
Future<void> openPage(String name) async {
|
||||
final finder = findPageName(name);
|
||||
expect(finder, findsOneWidget);
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
/// Tap the ... button beside the page name.
|
||||
///
|
||||
/// Must call [hoverOnPageName] first.
|
||||
@@ -230,6 +245,42 @@ extension CommonOperations on WidgetTester {
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> simulateKeyEvent(
|
||||
LogicalKeyboardKey key, {
|
||||
bool isControlPressed = false,
|
||||
bool isShiftPressed = false,
|
||||
bool isAltPressed = false,
|
||||
bool isMetaPressed = false,
|
||||
}) async {
|
||||
if (isControlPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.control);
|
||||
}
|
||||
if (isShiftPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.shift);
|
||||
}
|
||||
if (isAltPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.alt);
|
||||
}
|
||||
if (isMetaPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.meta);
|
||||
}
|
||||
await simulateKeyDownEvent(key);
|
||||
await simulateKeyUpEvent(key);
|
||||
if (isControlPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.control);
|
||||
}
|
||||
if (isShiftPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
|
||||
}
|
||||
if (isAltPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.alt);
|
||||
}
|
||||
if (isMetaPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.meta);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
|
||||
@@ -1,20 +1,47 @@
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/create_filter_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -22,7 +49,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
@@ -30,14 +56,44 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'common_operations.dart';
|
||||
import 'expectation.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'mock/mock_file_picker.dart';
|
||||
|
||||
extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> openV020database() async {
|
||||
await initializeAppFlowy();
|
||||
await tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
expectToSeePageName(readme);
|
||||
|
||||
await tapAddButton();
|
||||
await tapImportButton();
|
||||
|
||||
final testFileNames = ['v020.afdb'];
|
||||
final fileLocation = await currentFileLocation();
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/database',
|
||||
fileName,
|
||||
),
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: 'import_files');
|
||||
await tapDatabaseRawDataButton();
|
||||
await openPage('v020');
|
||||
}
|
||||
|
||||
Future<void> hoverOnFirstRowOfGrid() async {
|
||||
final findRow = find.byType(GridRow);
|
||||
expect(findRow, findsWidgets);
|
||||
@@ -51,29 +107,27 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
required FieldType fieldType,
|
||||
required String input,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(fieldType);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
final cell = cellFinder(rowIndex, fieldType);
|
||||
|
||||
expect(cell, findsOneWidget);
|
||||
await enterText(cell, input);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Finder cellFinder(int rowIndex, FieldType fieldType) {
|
||||
final findRow = find.byType(GridRow, skipOffstage: false);
|
||||
final findCell = finderForFieldType(fieldType);
|
||||
return find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
skipOffstage: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tapCheckboxCellInGrid({
|
||||
required int rowIndex,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(FieldType.Checkbox);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
final cell = cellFinder(rowIndex, FieldType.Checkbox);
|
||||
|
||||
final button = find.descendant(
|
||||
of: cell,
|
||||
@@ -88,14 +142,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
required int rowIndex,
|
||||
required bool isSelected,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(FieldType.Checkbox);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
final cell = cellFinder(rowIndex, FieldType.Checkbox);
|
||||
var finder = find.byType(CheckboxCellUncheck);
|
||||
if (isSelected) {
|
||||
finder = find.byType(CheckboxCellCheck);
|
||||
@@ -114,36 +161,118 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(fieldType);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
final cell = cellFinder(rowIndex, fieldType);
|
||||
expect(cell, findsOneWidget);
|
||||
await tapButton(cell);
|
||||
await tapButton(cell, warnIfMissed: false);
|
||||
}
|
||||
|
||||
/// The [fieldName] must be uqniue in the grid.
|
||||
Future<void> assertCellContent({
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
required String content,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(fieldType);
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
final findCell = cellFinder(rowIndex, fieldType);
|
||||
final findContent = find.descendant(
|
||||
of: cell,
|
||||
of: findCell,
|
||||
matching: find.text(content),
|
||||
skipOffstage: false,
|
||||
);
|
||||
|
||||
expect(findContent, findsOneWidget);
|
||||
final text = find.descendant(
|
||||
of: find.byType(TextField),
|
||||
matching: findContent,
|
||||
skipOffstage: false,
|
||||
);
|
||||
|
||||
expect(text, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> assertSingleSelectOption({
|
||||
required int rowIndex,
|
||||
required String content,
|
||||
}) async {
|
||||
final findCell = cellFinder(rowIndex, FieldType.SingleSelect);
|
||||
if (content.isNotEmpty) {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTag && widget.name == content,
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> assertMultiSelectOption({
|
||||
required int rowIndex,
|
||||
required List<String> contents,
|
||||
}) async {
|
||||
final findCell = cellFinder(rowIndex, FieldType.MultiSelect);
|
||||
for (final content in contents) {
|
||||
if (content.isNotEmpty) {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTag && widget.name == content,
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> assertChecklistCellInGrid({
|
||||
required int rowIndex,
|
||||
required double percent,
|
||||
}) async {
|
||||
final findCell = cellFinder(rowIndex, FieldType.Checklist);
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is ChecklistProgressBar) {
|
||||
return widget.percent == percent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> assertDateCellInGrid({
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
required String content,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow, skipOffstage: false);
|
||||
final findCell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is GridDateCell && widget.fieldType == fieldType,
|
||||
),
|
||||
skipOffstage: false,
|
||||
);
|
||||
|
||||
final dateCellText = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byType(GridDateCellText),
|
||||
);
|
||||
|
||||
final text = find.descendant(
|
||||
of: dateCellText,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is FlowyText) {
|
||||
return widget.text == content;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
skipOffstage: false,
|
||||
);
|
||||
expect(text, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> selectDay({
|
||||
@@ -160,6 +289,59 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> tapSelectOptionCellInGrid({
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
}) async {
|
||||
assert(
|
||||
fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect,
|
||||
);
|
||||
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(fieldType);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
await tapButton(cell);
|
||||
}
|
||||
|
||||
/// The [SelectOptionCellEditor] must be opened first.
|
||||
Future<void> createOption({
|
||||
required String name,
|
||||
}) async {
|
||||
final findEditor = find.byType(SelectOptionCellEditor);
|
||||
expect(findEditor, findsOneWidget);
|
||||
|
||||
final findTextField = find.byType(SelectOptionTextField);
|
||||
expect(findTextField, findsOneWidget);
|
||||
|
||||
await enterText(findTextField, name);
|
||||
await pump();
|
||||
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> findSelectOptionWithNameInGrid({
|
||||
required int rowIndex,
|
||||
required String name,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final option = find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTag && widget.name == name,
|
||||
);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: option,
|
||||
);
|
||||
|
||||
expect(cell, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
@@ -290,8 +472,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
/// Must call [tapTypeOptionButton] first.
|
||||
Future<void> selectFieldType(FieldType fieldType) async {
|
||||
final fieldTypeButton = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == fieldType.title(),
|
||||
final fieldTypeCell = find.byType(FieldTypeCell);
|
||||
final fieldTypeButton = find.descendant(
|
||||
of: fieldTypeCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == fieldType.title(),
|
||||
),
|
||||
);
|
||||
await tapButton(fieldTypeButton);
|
||||
}
|
||||
@@ -308,7 +494,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> assertNumberOfRowsInGridPage(int num) async {
|
||||
expect(find.byType(GridRow), findsNWidgets(num));
|
||||
expect(
|
||||
find.byType(GridRow, skipOffstage: false),
|
||||
findsNWidgets(num),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> assertDocumentExistInRowDetailPage() async {
|
||||
@@ -353,9 +542,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
Future<void> dismissFieldEditor() async {
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle();
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Future<void> findFieldEditor(dynamic matcher) async {
|
||||
@@ -368,6 +555,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(finder, matcher);
|
||||
}
|
||||
|
||||
Future<void> findSelectOptionEditor(dynamic matcher) async {
|
||||
final finder = find.byType(SelectOptionCellEditor);
|
||||
expect(finder, matcher);
|
||||
}
|
||||
|
||||
Future<void> dismissSelectOptionEditor() async {
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapCreateRowButtonInGrid() async {
|
||||
await tapButton(find.byType(GridAddRowButton));
|
||||
}
|
||||
@@ -387,7 +584,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
Future<void> assertRowCountInGridPage(int num) async {
|
||||
final text = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == rowCountString(num),
|
||||
(widget) => widget is FlowyText && widget.text == rowCountString(num),
|
||||
);
|
||||
expect(text, findsOneWidget);
|
||||
}
|
||||
@@ -405,13 +602,201 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(find.byType(SettingButton));
|
||||
}
|
||||
|
||||
Future<void> tapDatabaseFilterButton() async {
|
||||
await tapButton(find.byType(FilterButton));
|
||||
}
|
||||
|
||||
Future<void> tapDatabaseSortButton() async {
|
||||
await tapButton(find.byType(SortButton));
|
||||
}
|
||||
|
||||
Future<void> tapCreateFilterByFieldType(
|
||||
FieldType fieldType,
|
||||
String title,
|
||||
) async {
|
||||
final findFilter = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is GridFilterPropertyCell &&
|
||||
widget.fieldInfo.fieldType == fieldType &&
|
||||
widget.fieldInfo.name == title,
|
||||
);
|
||||
|
||||
await tapButton(findFilter);
|
||||
}
|
||||
|
||||
Future<void> tapFilterButtonInGrid(String filterName) async {
|
||||
final findFilter = find.byType(FilterMenuItem);
|
||||
final button = find.descendant(
|
||||
of: findFilter,
|
||||
matching: find.text(filterName),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapCreateSortByFieldType(
|
||||
FieldType fieldType,
|
||||
String title,
|
||||
) async {
|
||||
final findSort = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is GridSortPropertyCell &&
|
||||
widget.fieldInfo.fieldType == fieldType &&
|
||||
widget.fieldInfo.name == title,
|
||||
);
|
||||
|
||||
await tapButton(findSort);
|
||||
}
|
||||
|
||||
// Must call [tapSortMenuInSettingBar] first.
|
||||
Future<void> tapCreateSortByFieldTypeInSortMenu(
|
||||
FieldType fieldType,
|
||||
String title,
|
||||
) async {
|
||||
await tapButton(find.byType(DatabaseAddSortButton));
|
||||
|
||||
final findSort = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is GridSortPropertyCell &&
|
||||
widget.fieldInfo.fieldType == fieldType &&
|
||||
widget.fieldInfo.name == title,
|
||||
);
|
||||
|
||||
await tapButton(findSort);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapSortMenuInSettingBar() async {
|
||||
await tapButton(find.byType(SortMenu));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Must call [tapSortMenuInSettingBar] first.
|
||||
Future<void> tapSortButtonByName(String name) async {
|
||||
final findSortItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is DatabaseSortItem && widget.sortInfo.fieldInfo.name == name,
|
||||
);
|
||||
await tapButton(findSortItem);
|
||||
}
|
||||
|
||||
/// Must call [tapSortButtonByName] first.
|
||||
Future<void> tapSortByDescending() async {
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(OrderPannelItem),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.text == LocaleKeys.grid_sort_descending.tr(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Must call [tapSortMenuInSettingBar] first.
|
||||
Future<void> tapAllSortButton() async {
|
||||
await tapButton(find.byType(DatabaseDeleteSortButton));
|
||||
}
|
||||
|
||||
Future<void> scrollOptionFilterListByOffset(Offset offset) async {
|
||||
await drag(find.byType(SelectOptionFilterEditor), offset);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> enterTextInTextFilter(String text) async {
|
||||
final findEditor = find.byType(TextFilterEditor);
|
||||
final findTextField = find.descendant(
|
||||
of: findEditor,
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
|
||||
await enterText(findTextField, text);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> tapDisclosureButtonInFinder(Finder finder) async {
|
||||
final findDisclosure = find.descendant(
|
||||
of: finder,
|
||||
matching: find.byType(DisclosureButton),
|
||||
);
|
||||
|
||||
await tapButton(findDisclosure);
|
||||
}
|
||||
|
||||
/// must call [tapDisclosureButtonInFinder] first.
|
||||
Future<void> tapDeleteFilterButtonInGrid() async {
|
||||
await tapButton(find.text(LocaleKeys.grid_settings_deleteFilter.tr()));
|
||||
}
|
||||
|
||||
Future<void> tapCheckboxFilterButtonInGrid() async {
|
||||
await tapButton(find.byType(CheckboxFilterConditionList));
|
||||
}
|
||||
|
||||
Future<void> tapChecklistFilterButtonInGrid() async {
|
||||
await tapButton(find.byType(ChecklistFilterConditionList));
|
||||
}
|
||||
|
||||
/// The [SelectOptionFilterList] must show up first.
|
||||
Future<void> tapOptionFilterWithName(String name) async {
|
||||
final findCell = find.descendant(
|
||||
of: find.byType(SelectOptionFilterList),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SelectOptionFilterCell && widget.option.name == name,
|
||||
skipOffstage: false,
|
||||
),
|
||||
skipOffstage: false,
|
||||
);
|
||||
expect(findCell, findsOneWidget);
|
||||
await tapButton(findCell, warnIfMissed: false);
|
||||
}
|
||||
|
||||
Future<void> tapCheckedButtonOnCheckboxFilter() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapUnCheckedButtonOnCheckboxFilter() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(LocaleKeys.grid_checkboxFilter_isUnchecked.tr()),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapCompletedButtonOnChecklistFilter() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(LocaleKeys.grid_checklistFilter_isComplete.tr()),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapUnCompletedButtonOnChecklistFilter() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapDatabaseLayoutButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingItem);
|
||||
final findLayoutButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.title == DatabaseSettingAction.showLayout.title(),
|
||||
widget.text == DatabaseSettingAction.showLayout.title(),
|
||||
);
|
||||
|
||||
final button = find.descendant(
|
||||
@@ -422,10 +807,156 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapCalendarLayoutSettingButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingItem);
|
||||
final findLayoutButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.text == DatabaseSettingAction.showCalendarLayout.title(),
|
||||
);
|
||||
|
||||
final button = find.descendant(
|
||||
of: findSettingItem,
|
||||
matching: findLayoutButton,
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapFirstDayOfWeek() async {
|
||||
await tapButton(find.byType(FirstDayOfWeek));
|
||||
}
|
||||
|
||||
Future<void> tapFirstDayOfWeekStartFromSunday() async {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) => widget is StartFromButton && widget.dayIndex == 0,
|
||||
);
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> tapFirstDayOfWeekStartFromMonday() async {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) => widget is StartFromButton && widget.dayIndex == 1,
|
||||
);
|
||||
await tapButton(finder);
|
||||
|
||||
// Dismiss the popover overlay in cause of obscure the tapButton
|
||||
// in the next test case.
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
void assertFirstDayOfWeekStartFromMonday() {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is StartFromButton &&
|
||||
widget.dayIndex == 1 &&
|
||||
widget.isSelected == true,
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
|
||||
void assertFirstDayOfWeekStartFromSunday() {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is StartFromButton &&
|
||||
widget.dayIndex == 0 &&
|
||||
widget.isSelected == true,
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
|
||||
final findAddButton = find.byType(AddDatabaseViewButton);
|
||||
await tapButton(findAddButton);
|
||||
|
||||
final findCreateButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is TarBarAddButtonActionCell && widget.action == action,
|
||||
);
|
||||
await tapButton(findCreateButton);
|
||||
}
|
||||
|
||||
Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is TabBarItemButton && widget.view.layout == layout,
|
||||
);
|
||||
}
|
||||
|
||||
Finder findTabBarLinkViewByViewName(String name) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is TabBarItemButton && widget.view.name == name,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> renameLinkedView(Finder linkedView, String name) async {
|
||||
await tap(linkedView, buttons: kSecondaryButton);
|
||||
await pumpAndSettle();
|
||||
|
||||
await tapButton(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ActionCellWidget &&
|
||||
widget.action == TabBarViewAction.rename,
|
||||
),
|
||||
);
|
||||
|
||||
await enterText(
|
||||
find.descendant(
|
||||
of: find.byType(FlowyFormTextInput),
|
||||
matching: find.byType(TextFormField),
|
||||
),
|
||||
name,
|
||||
);
|
||||
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PrimaryTextButton &&
|
||||
widget.label == LocaleKeys.button_OK.tr(),
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> deleteDatebaseView(Finder linkedView) async {
|
||||
await tap(linkedView, buttons: kSecondaryButton);
|
||||
await pumpAndSettle();
|
||||
|
||||
await tapButton(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ActionCellWidget &&
|
||||
widget.action == TabBarViewAction.delete,
|
||||
),
|
||||
);
|
||||
|
||||
final okButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PrimaryTextButton &&
|
||||
widget.label == LocaleKeys.button_OK.tr(),
|
||||
);
|
||||
await tapButton(okButton);
|
||||
}
|
||||
|
||||
Future<void> assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) async {
|
||||
switch (layout) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
expect(find.byType(BoardPage), findsOneWidget);
|
||||
break;
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
expect(find.byType(CalendarPage), findsOneWidget);
|
||||
break;
|
||||
case DatabaseLayoutPB.Grid:
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unknown database layout type: $layout');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
||||
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
|
||||
final findText = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == layout.layoutName(),
|
||||
(widget) => widget is FlowyText && widget.text == layout.layoutName(),
|
||||
);
|
||||
|
||||
final button = find.descendant(
|
||||
@@ -439,6 +970,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
||||
expect(finderForDatabaseLayoutType(layout), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> tapDatabaseRawDataButton() async {
|
||||
await tapButtonWithName(LocaleKeys.importPanel_database.tr());
|
||||
}
|
||||
}
|
||||
|
||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
|
||||
@@ -457,24 +992,24 @@ Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
|
||||
Finder finderForFieldType(FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return find.byType(GridCheckboxCell);
|
||||
return find.byType(GridCheckboxCell, skipOffstage: false);
|
||||
case FieldType.DateTime:
|
||||
return find.byType(GridDateCell);
|
||||
return find.byType(GridDateCell, skipOffstage: false);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return find.byType(GridDateCell);
|
||||
return find.byType(GridDateCell, skipOffstage: false);
|
||||
case FieldType.SingleSelect:
|
||||
return find.byType(GridSingleSelectCell);
|
||||
return find.byType(GridSingleSelectCell, skipOffstage: false);
|
||||
case FieldType.MultiSelect:
|
||||
return find.byType(GridMultiSelectCell);
|
||||
return find.byType(GridMultiSelectCell, skipOffstage: false);
|
||||
case FieldType.Checklist:
|
||||
return find.byType(GridChecklistCell);
|
||||
return find.byType(GridChecklistCell, skipOffstage: false);
|
||||
case FieldType.Number:
|
||||
return find.byType(GridNumberCell);
|
||||
return find.byType(GridNumberCell, skipOffstage: false);
|
||||
case FieldType.RichText:
|
||||
return find.byType(GridTextCell);
|
||||
return find.byType(GridTextCell, skipOffstage: false);
|
||||
case FieldType.URL:
|
||||
return find.byType(GridURLCell);
|
||||
return find.byType(GridURLCell, skipOffstage: false);
|
||||
default:
|
||||
throw Exception('Unknown field type: $fieldType');
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ class EditorOperations {
|
||||
|
||||
final WidgetTester tester;
|
||||
|
||||
EditorState getCurrentEditorState() {
|
||||
return tester
|
||||
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
|
||||
.editorState;
|
||||
}
|
||||
|
||||
/// Tap the line of editor at [index]
|
||||
Future<void> tapLineOfEditorAt(int index) async {
|
||||
final textBlocks = find.byType(TextBlockComponentWidget);
|
||||
|
||||
@@ -12,7 +12,7 @@ extension Expectation on WidgetTester {
|
||||
/// Expect to see the home page and with a default read me page.
|
||||
void expectToSeeHomePage() {
|
||||
expect(find.byType(HomeStack), findsOneWidget);
|
||||
expect(find.textContaining(readme), findsOneWidget);
|
||||
expect(find.textContaining(readme), findsWidgets);
|
||||
}
|
||||
|
||||
/// Expect to see the page name on the home page.
|
||||
@@ -42,7 +42,7 @@ extension Expectation on WidgetTester {
|
||||
final exportSuccess = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.title == LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
expect(exportSuccess, findsOneWidget);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ extension Expectation on WidgetTester {
|
||||
/// Expect to see the user name on the home page
|
||||
void expectToSeeUserName(String name) {
|
||||
final userName = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == name,
|
||||
(widget) => widget is FlowyText && widget.text == name,
|
||||
);
|
||||
expect(userName, findsOneWidget);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ extension Expectation on WidgetTester {
|
||||
Finder textWidget = find.textContaining(text, findRichText: true);
|
||||
if (textWidget.evaluate().isEmpty) {
|
||||
textWidget = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == text,
|
||||
(widget) => widget is FlowyText && widget.text == text,
|
||||
);
|
||||
}
|
||||
expect(textWidget, findsOneWidget);
|
||||
@@ -82,6 +82,7 @@ extension Expectation on WidgetTester {
|
||||
Finder findPageName(String name) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is ViewSectionItem && widget.view.name == name,
|
||||
skipOffstage: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,8 @@ import 'startup/startup.dart';
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await FlowyRunner.run(FlowyApp());
|
||||
await FlowyRunner.run(
|
||||
FlowyApp(),
|
||||
integrationEnv(),
|
||||
);
|
||||
}
|
||||
+109
-89
@@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart';
|
||||
@@ -15,6 +15,7 @@ import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'database_view_service.dart';
|
||||
import 'defines.dart';
|
||||
import 'layout/layout_service.dart';
|
||||
import 'layout/layout_setting_listener.dart';
|
||||
import 'row/row_cache.dart';
|
||||
import 'group/group_listener.dart';
|
||||
@@ -49,16 +50,11 @@ class DatabaseLayoutSettingCallbacks {
|
||||
});
|
||||
}
|
||||
|
||||
class CalendarLayoutCallbacks {
|
||||
final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged;
|
||||
|
||||
CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
|
||||
}
|
||||
|
||||
class DatabaseCallbacks {
|
||||
OnDatabaseChanged? onDatabaseChanged;
|
||||
OnFieldsChanged? onFieldsChanged;
|
||||
OnFiltersChanged? onFiltersChanged;
|
||||
OnSortsChanged? onSortsChanged;
|
||||
OnNumOfRowsChanged? onNumOfRowsChanged;
|
||||
OnRowsDeleted? onRowsDeleted;
|
||||
OnRowsUpdated? onRowsUpdated;
|
||||
@@ -69,6 +65,7 @@ class DatabaseCallbacks {
|
||||
this.onNumOfRowsChanged,
|
||||
this.onFieldsChanged,
|
||||
this.onFiltersChanged,
|
||||
this.onSortsChanged,
|
||||
this.onRowsUpdated,
|
||||
this.onRowsDeleted,
|
||||
this.onRowsCreated,
|
||||
@@ -79,15 +76,14 @@ class DatabaseController {
|
||||
final String viewId;
|
||||
final DatabaseViewBackendService _databaseViewBackendSvc;
|
||||
final FieldController fieldController;
|
||||
DatabaseLayoutPB? databaseLayout;
|
||||
DatabaseLayoutPB databaseLayout;
|
||||
DatabaseLayoutSettingPB? databaseLayoutSetting;
|
||||
late DatabaseViewCache _viewCache;
|
||||
|
||||
// Callbacks
|
||||
DatabaseCallbacks? _databaseCallbacks;
|
||||
GroupCallbacks? _groupCallbacks;
|
||||
DatabaseLayoutSettingCallbacks? _layoutCallbacks;
|
||||
CalendarLayoutCallbacks? _calendarLayoutCallbacks;
|
||||
final List<DatabaseCallbacks> _databaseCallbacks = [];
|
||||
final List<GroupCallbacks> _groupCallbacks = [];
|
||||
final List<DatabaseLayoutSettingCallbacks> _layoutCallbacks = [];
|
||||
|
||||
// Getters
|
||||
RowCache get rowCache => _viewCache.rowCache;
|
||||
@@ -95,15 +91,14 @@ class DatabaseController {
|
||||
// Listener
|
||||
final DatabaseGroupListener _groupListener;
|
||||
final DatabaseLayoutSettingListener _layoutListener;
|
||||
final DatabaseCalendarLayoutListener _calendarLayoutListener;
|
||||
|
||||
DatabaseController({required ViewPB view})
|
||||
: viewId = view.id,
|
||||
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
|
||||
fieldController = FieldController(viewId: view.id),
|
||||
_groupListener = DatabaseGroupListener(view.id),
|
||||
_layoutListener = DatabaseLayoutSettingListener(view.id),
|
||||
_calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
|
||||
databaseLayout = databaseLayoutFromViewLayout(view.layout),
|
||||
_layoutListener = DatabaseLayoutSettingListener(view.id) {
|
||||
_viewCache = DatabaseViewCache(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
@@ -114,44 +109,51 @@ class DatabaseController {
|
||||
_listenOnLayoutChanged();
|
||||
}
|
||||
|
||||
void setListener({
|
||||
void addListener({
|
||||
DatabaseCallbacks? onDatabaseChanged,
|
||||
DatabaseLayoutSettingCallbacks? onLayoutChanged,
|
||||
GroupCallbacks? onGroupChanged,
|
||||
CalendarLayoutCallbacks? onCalendarLayoutChanged,
|
||||
}) {
|
||||
_layoutCallbacks = onLayoutChanged;
|
||||
_databaseCallbacks = onDatabaseChanged;
|
||||
_groupCallbacks = onGroupChanged;
|
||||
_calendarLayoutCallbacks = onCalendarLayoutChanged;
|
||||
if (onLayoutChanged != null) {
|
||||
_layoutCallbacks.add(onLayoutChanged);
|
||||
}
|
||||
|
||||
if (onDatabaseChanged != null) {
|
||||
_databaseCallbacks.add(onDatabaseChanged);
|
||||
}
|
||||
|
||||
if (onGroupChanged != null) {
|
||||
_groupCallbacks.add(onGroupChanged);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> open() async {
|
||||
return _databaseViewBackendSvc.openGrid().then((result) {
|
||||
return _databaseViewBackendSvc.openDatabase().then((result) {
|
||||
return result.fold(
|
||||
(database) async {
|
||||
(DatabasePB database) async {
|
||||
databaseLayout = database.layoutType;
|
||||
|
||||
if (databaseLayout == DatabaseLayoutPB.Calendar) {
|
||||
_listenOnCalendarLayoutChanged();
|
||||
}
|
||||
|
||||
_databaseCallbacks?.onDatabaseChanged?.call(database);
|
||||
_viewCache.rowCache.setInitialRows(database.rows);
|
||||
return await fieldController
|
||||
.loadFields(
|
||||
// Load the actual database field data.
|
||||
final fieldsOrFail = await fieldController.loadFields(
|
||||
fieldIds: database.fields,
|
||||
)
|
||||
.then(
|
||||
(result) {
|
||||
return result.fold(
|
||||
(l) => Future(() async {
|
||||
await _loadGroups();
|
||||
await _loadLayoutSetting();
|
||||
return left(l);
|
||||
}),
|
||||
(err) => right(err),
|
||||
);
|
||||
);
|
||||
return fieldsOrFail.fold(
|
||||
(fields) {
|
||||
// Notify the database is changed after the fields are loaded.
|
||||
// The database won't can't be used until the fields are loaded.
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onDatabaseChanged?.call(database);
|
||||
}
|
||||
_viewCache.rowCache.setInitialRows(database.rows);
|
||||
return Future(() async {
|
||||
await _loadGroups();
|
||||
await _loadLayoutSetting();
|
||||
return left(fields);
|
||||
});
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return right(err);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -212,11 +214,14 @@ class DatabaseController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateCalenderLayoutSetting(
|
||||
CalendarLayoutSettingPB layoutSetting,
|
||||
Future<void> updateLayoutSetting(
|
||||
CalendarLayoutSettingPB calendarlLayoutSetting,
|
||||
) async {
|
||||
await _databaseViewBackendSvc
|
||||
.updateLayoutSetting(calendarLayoutSetting: layoutSetting)
|
||||
.updateLayoutSetting(
|
||||
calendarLayoutSetting: calendarlLayoutSetting,
|
||||
layoutType: databaseLayout,
|
||||
)
|
||||
.then((result) {
|
||||
result.fold((l) => null, (r) => Log.error(r));
|
||||
});
|
||||
@@ -227,10 +232,9 @@ class DatabaseController {
|
||||
await fieldController.dispose();
|
||||
await _groupListener.stop();
|
||||
await _viewCache.dispose();
|
||||
_databaseCallbacks = null;
|
||||
_groupCallbacks = null;
|
||||
_layoutCallbacks = null;
|
||||
_calendarLayoutCallbacks = null;
|
||||
_databaseCallbacks.clear();
|
||||
_groupCallbacks.clear();
|
||||
_layoutCallbacks.clear();
|
||||
}
|
||||
|
||||
Future<void> _loadGroups() async {
|
||||
@@ -238,7 +242,9 @@ class DatabaseController {
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(groups) {
|
||||
_groupCallbacks?.onGroupByField?.call(groups.items);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onGroupByField?.call(groups.items);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
),
|
||||
@@ -246,46 +252,63 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
Future<void> _loadLayoutSetting() async {
|
||||
if (databaseLayout != null) {
|
||||
_databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) {
|
||||
result.fold(
|
||||
(newDatabaseLayoutSetting) {
|
||||
databaseLayoutSetting = newDatabaseLayoutSetting;
|
||||
databaseLayoutSetting?.freeze();
|
||||
_databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) {
|
||||
result.fold(
|
||||
(newDatabaseLayoutSetting) {
|
||||
databaseLayoutSetting = newDatabaseLayoutSetting;
|
||||
databaseLayoutSetting?.freeze();
|
||||
|
||||
_layoutCallbacks?.onLoadLayout(newDatabaseLayoutSetting);
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
for (final callback in _layoutCallbacks) {
|
||||
callback.onLoadLayout(newDatabaseLayoutSetting);
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _listenOnRowsChanged() {
|
||||
final callbacks = DatabaseViewCallbacks(
|
||||
onNumOfRowsChanged: (rows, rowByRowId, reason) {
|
||||
_databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
|
||||
}
|
||||
},
|
||||
onRowsDeleted: (ids) {
|
||||
_databaseCallbacks?.onRowsDeleted?.call(ids);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onRowsDeleted?.call(ids);
|
||||
}
|
||||
},
|
||||
onRowsUpdated: (ids, reason) {
|
||||
_databaseCallbacks?.onRowsUpdated?.call(ids, reason);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onRowsUpdated?.call(ids, reason);
|
||||
}
|
||||
},
|
||||
onRowsCreated: (ids) {
|
||||
_databaseCallbacks?.onRowsCreated?.call(ids);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onRowsCreated?.call(ids);
|
||||
}
|
||||
},
|
||||
);
|
||||
_viewCache.setListener(callbacks);
|
||||
_viewCache.addListener(callbacks);
|
||||
}
|
||||
|
||||
void _listenOnFieldsChanged() {
|
||||
fieldController.addListener(
|
||||
onReceiveFields: (fields) {
|
||||
_databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields));
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onFieldsChanged?.call(UnmodifiableListView(fields));
|
||||
}
|
||||
},
|
||||
onSorts: (sorts) {
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onSortsChanged?.call(sorts);
|
||||
}
|
||||
},
|
||||
onFilters: (filters) {
|
||||
_databaseCallbacks?.onFiltersChanged?.call(filters);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onFiltersChanged?.call(filters);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -296,15 +319,21 @@ class DatabaseController {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
if (changeset.updateGroups.isNotEmpty) {
|
||||
_groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onUpdateGroup?.call(changeset.updateGroups);
|
||||
}
|
||||
}
|
||||
|
||||
if (changeset.deletedGroups.isNotEmpty) {
|
||||
_groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onDeleteGroup?.call(changeset.deletedGroups);
|
||||
}
|
||||
}
|
||||
|
||||
for (final insertedGroup in changeset.insertedGroups) {
|
||||
_groupCallbacks?.onInsertGroup?.call(insertedGroup);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onInsertGroup?.call(insertedGroup);
|
||||
}
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
@@ -313,7 +342,9 @@ class DatabaseController {
|
||||
onGroupByNewField: (result) {
|
||||
result.fold(
|
||||
(groups) {
|
||||
_groupCallbacks?.onGroupByField?.call(groups);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onGroupByField?.call(groups);
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
@@ -325,24 +356,13 @@ class DatabaseController {
|
||||
_layoutListener.start(
|
||||
onLayoutChanged: (result) {
|
||||
result.fold(
|
||||
(newDatabaseLayoutSetting) {
|
||||
databaseLayoutSetting = newDatabaseLayoutSetting;
|
||||
(newLayout) {
|
||||
databaseLayoutSetting = newLayout;
|
||||
databaseLayoutSetting?.freeze();
|
||||
|
||||
_layoutCallbacks?.onLayoutChanged(newDatabaseLayoutSetting);
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _listenOnCalendarLayoutChanged() {
|
||||
_calendarLayoutListener.start(
|
||||
onCalendarLayoutChanged: (result) {
|
||||
result.fold(
|
||||
(l) {
|
||||
_calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
|
||||
for (final callback in _layoutCallbacks) {
|
||||
callback.onLayoutChanged(newLayout);
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
|
||||
+5
-2
@@ -25,7 +25,7 @@ class DatabaseViewBackendService {
|
||||
.then((value) => value.leftMap((l) => l.value));
|
||||
}
|
||||
|
||||
Future<Either<DatabasePB, FlowyError>> openGrid() async {
|
||||
Future<Either<DatabasePB, FlowyError>> openDatabase() async {
|
||||
final payload = DatabaseViewIdPB(value: viewId);
|
||||
return DatabaseEventGetDatabase(payload).send();
|
||||
}
|
||||
@@ -113,9 +113,12 @@ class DatabaseViewBackendService {
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateLayoutSetting({
|
||||
required DatabaseLayoutPB layoutType,
|
||||
CalendarLayoutSettingPB? calendarLayoutSetting,
|
||||
}) {
|
||||
final payload = LayoutSettingChangesetPB.create()..viewId = viewId;
|
||||
final payload = LayoutSettingChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..layoutType = layoutType;
|
||||
if (calendarLayoutSetting != null) {
|
||||
payload.calendar = calendarLayoutSetting;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
|
||||
@@ -10,6 +11,7 @@ import 'row/row_service.dart';
|
||||
|
||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
||||
typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
||||
typedef OnSortsChanged = void Function(List<SortInfo>);
|
||||
typedef OnDatabaseChanged = void Function(DatabasePB);
|
||||
|
||||
typedef OnRowsCreated = void Function(List<RowId> ids);
|
||||
|
||||
+50
-23
@@ -33,7 +33,8 @@ class _GridFieldNotifier extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<FieldInfo> get fieldInfos => _fieldInfos;
|
||||
UnmodifiableListView<FieldInfo> get fieldInfos =>
|
||||
UnmodifiableListView(_fieldInfos);
|
||||
}
|
||||
|
||||
class _GridFilterNotifier extends ChangeNotifier {
|
||||
@@ -85,9 +86,11 @@ class FieldController {
|
||||
final FilterBackendService _filterBackendSvc;
|
||||
final SortBackendService _sortBackendSvc;
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
// Field callbacks
|
||||
final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
|
||||
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
|
||||
final _GridFieldNotifier _fieldNotifier = _GridFieldNotifier();
|
||||
|
||||
// Field updated callbacks
|
||||
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
|
||||
@@ -107,15 +110,15 @@ class FieldController {
|
||||
final Map<String, SortPB> _sortPBByFieldId = {};
|
||||
|
||||
// Getters
|
||||
List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
|
||||
List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];
|
||||
List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
|
||||
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
|
||||
|
||||
FieldInfo? getField(String fieldId) {
|
||||
final fields = _fieldNotifier?.fieldInfos
|
||||
.where((element) => element.id == fieldId)
|
||||
.toList() ??
|
||||
[];
|
||||
final fields = _fieldNotifier.fieldInfos
|
||||
.where((element) => element.id == fieldId)
|
||||
.toList();
|
||||
|
||||
if (fields.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
@@ -169,6 +172,10 @@ class FieldController {
|
||||
_listenOnSortChanged();
|
||||
|
||||
_settingBackendSvc.getSetting().then((result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(setting) => _updateSetting(setting),
|
||||
(err) => Log.error(err),
|
||||
@@ -257,6 +264,10 @@ class FieldController {
|
||||
|
||||
_filtersListener.start(
|
||||
onFilterChanged: (result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(FilterChangesetNotificationPB changeset) {
|
||||
final List<FilterInfo> filters = filterInfos;
|
||||
@@ -351,6 +362,9 @@ class FieldController {
|
||||
|
||||
_sortsListener.start(
|
||||
onSortChanged: (result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
result.fold(
|
||||
(SortChangesetNotificationPB changeset) {
|
||||
final List<SortInfo> newSortInfos = sortInfos;
|
||||
@@ -371,6 +385,10 @@ class FieldController {
|
||||
//Listen on setting changes
|
||||
_settingListener.start(
|
||||
onSettingUpdated: (result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(setting) => _updateSetting(setting),
|
||||
(r) => Log.error(r),
|
||||
@@ -385,6 +403,9 @@ class FieldController {
|
||||
onFieldsChanged: (result) {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
_deleteFields(changeset.deletedFields);
|
||||
_insertFields(changeset.insertedFields);
|
||||
|
||||
@@ -417,27 +438,29 @@ class FieldController {
|
||||
}
|
||||
|
||||
void _updateFieldInfos() {
|
||||
if (_fieldNotifier != null) {
|
||||
for (final field in _fieldNotifier!.fieldInfos) {
|
||||
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
|
||||
field._hasFilter = _filterPBByFieldId[field.id] != null;
|
||||
field._hasSort = _sortPBByFieldId[field.id] != null;
|
||||
}
|
||||
_fieldNotifier?.notify();
|
||||
for (final field in _fieldNotifier.fieldInfos) {
|
||||
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
|
||||
field._hasFilter = _filterPBByFieldId[field.id] != null;
|
||||
field._hasSort = _sortPBByFieldId[field.id] != null;
|
||||
}
|
||||
_fieldNotifier.notify();
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
if (_isDisposed) {
|
||||
Log.warn('FieldController is already disposed');
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
await _fieldListener.stop();
|
||||
await _filtersListener.stop();
|
||||
await _settingListener.stop();
|
||||
await _sortsListener.stop();
|
||||
|
||||
for (final callback in _fieldCallbacks.values) {
|
||||
_fieldNotifier?.removeListener(callback);
|
||||
_fieldNotifier.removeListener(callback);
|
||||
}
|
||||
_fieldNotifier?.dispose();
|
||||
_fieldNotifier = null;
|
||||
_fieldNotifier.dispose();
|
||||
|
||||
for (final callback in _filterCallbacks.values) {
|
||||
_filterNotifier?.removeListener(callback);
|
||||
@@ -460,7 +483,11 @@ class FieldController {
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(newFields) {
|
||||
_fieldNotifier?.fieldInfos =
|
||||
if (_isDisposed) {
|
||||
return left(unit);
|
||||
}
|
||||
|
||||
_fieldNotifier.fieldInfos =
|
||||
newFields.map((field) => FieldInfo(field: field)).toList();
|
||||
_loadFilters();
|
||||
_loadSorts();
|
||||
@@ -551,7 +578,7 @@ class FieldController {
|
||||
}
|
||||
|
||||
_fieldCallbacks[onReceiveFields] = callback;
|
||||
_fieldNotifier?.addListener(callback);
|
||||
_fieldNotifier.addListener(callback);
|
||||
}
|
||||
|
||||
if (onFilters != null) {
|
||||
@@ -588,7 +615,7 @@ class FieldController {
|
||||
if (onFieldsListener != null) {
|
||||
final callback = _fieldCallbacks.remove(onFieldsListener);
|
||||
if (callback != null) {
|
||||
_fieldNotifier?.removeListener(callback);
|
||||
_fieldNotifier.removeListener(callback);
|
||||
}
|
||||
}
|
||||
if (onFiltersListener != null) {
|
||||
@@ -616,7 +643,7 @@ class FieldController {
|
||||
};
|
||||
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier?.fieldInfos = newFields;
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
|
||||
void _insertFields(List<IndexFieldPB> insertedFields) {
|
||||
@@ -632,7 +659,7 @@ class FieldController {
|
||||
newFieldInfos.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
_fieldNotifier?.fieldInfos = newFieldInfos;
|
||||
_fieldNotifier.fieldInfos = newFieldInfos;
|
||||
}
|
||||
|
||||
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
|
||||
@@ -654,7 +681,7 @@ class FieldController {
|
||||
}
|
||||
|
||||
if (updatedFields.isNotEmpty) {
|
||||
_fieldNotifier?.fieldInfos = newFields;
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
return updatedFields;
|
||||
}
|
||||
|
||||
+15
-2
@@ -15,13 +15,13 @@ class DatabaseLayoutBackendService {
|
||||
}) {
|
||||
final payload = UpdateViewPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..layout = _viewLayoutFromDatabaseLayout(layout);
|
||||
..layout = viewLayoutFromDatabaseLayout(layout);
|
||||
|
||||
return FolderEventUpdateView(payload).send();
|
||||
}
|
||||
}
|
||||
|
||||
ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
|
||||
ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
|
||||
switch (databaseLayout) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return ViewLayoutPB.Board;
|
||||
@@ -33,3 +33,16 @@ ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
|
||||
throw UnimplementedError;
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) {
|
||||
switch (viewLayout) {
|
||||
case ViewLayoutPB.Board:
|
||||
return DatabaseLayoutPB.Board;
|
||||
case ViewLayoutPB.Calendar:
|
||||
return DatabaseLayoutPB.Calendar;
|
||||
case ViewLayoutPB.Grid:
|
||||
return DatabaseLayoutPB.Grid;
|
||||
default:
|
||||
throw UnimplementedError;
|
||||
}
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
part 'setting_bloc.freezed.dart';
|
||||
|
||||
class DatabaseSettingBloc
|
||||
extends Bloc<DatabaseSettingEvent, DatabaseSettingState> {
|
||||
final String viewId;
|
||||
DatabaseSettingBloc({required this.viewId})
|
||||
: super(DatabaseSettingState.initial()) {
|
||||
on<DatabaseSettingEvent>(
|
||||
(event, emit) async {
|
||||
event.map(
|
||||
performAction: (_PerformAction value) {
|
||||
emit(state.copyWith(selectedAction: Some(value.action)));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DatabaseSettingEvent with _$DatabaseSettingEvent {
|
||||
const factory DatabaseSettingEvent.performAction(
|
||||
DatabaseSettingAction action,
|
||||
) = _PerformAction;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DatabaseSettingState with _$DatabaseSettingState {
|
||||
const factory DatabaseSettingState({
|
||||
required Option<DatabaseSettingAction> selectedAction,
|
||||
}) = _DatabaseSettingState;
|
||||
|
||||
factory DatabaseSettingState.initial() => DatabaseSettingState(
|
||||
selectedAction: none(),
|
||||
);
|
||||
}
|
||||
|
||||
enum DatabaseSettingAction {
|
||||
showProperties,
|
||||
showLayout,
|
||||
showGroup,
|
||||
showCalendarLayout,
|
||||
}
|
||||
|
||||
extension DatabaseSettingActionExtension on DatabaseSettingAction {
|
||||
String iconName() {
|
||||
switch (this) {
|
||||
case DatabaseSettingAction.showProperties:
|
||||
return 'grid/setting/properties';
|
||||
case DatabaseSettingAction.showLayout:
|
||||
return 'grid/setting/database_layout';
|
||||
case DatabaseSettingAction.showGroup:
|
||||
return 'grid/setting/group';
|
||||
case DatabaseSettingAction.showCalendarLayout:
|
||||
return 'grid/setting/calendar_layout';
|
||||
}
|
||||
}
|
||||
|
||||
String title() {
|
||||
switch (this) {
|
||||
case DatabaseSettingAction.showProperties:
|
||||
return LocaleKeys.grid_settings_Properties.tr();
|
||||
case DatabaseSettingAction.showLayout:
|
||||
return LocaleKeys.grid_settings_databaseLayout.tr();
|
||||
case DatabaseSettingAction.showGroup:
|
||||
return LocaleKeys.grid_settings_group.tr();
|
||||
case DatabaseSettingAction.showCalendarLayout:
|
||||
return LocaleKeys.calendar_settings_name.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of actions that should be shown for the given database layout.
|
||||
List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
|
||||
switch (layout) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return [
|
||||
DatabaseSettingAction.showProperties,
|
||||
DatabaseSettingAction.showLayout,
|
||||
DatabaseSettingAction.showGroup,
|
||||
];
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
return [
|
||||
DatabaseSettingAction.showProperties,
|
||||
DatabaseSettingAction.showLayout,
|
||||
DatabaseSettingAction.showCalendarLayout,
|
||||
];
|
||||
case DatabaseLayoutPB.Grid:
|
||||
return [
|
||||
DatabaseSettingAction.showProperties,
|
||||
DatabaseSettingAction.showLayout,
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'database_controller.dart';
|
||||
import 'database_view_service.dart';
|
||||
|
||||
part 'tar_bar_bloc.freezed.dart';
|
||||
|
||||
class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> {
|
||||
GridTabBarBloc({
|
||||
bool isInlineView = false,
|
||||
required ViewPB view,
|
||||
}) : super(GridTabBarState.initial(view)) {
|
||||
on<GridTabBarEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_listenInlineViewChanged();
|
||||
_loadChildView();
|
||||
},
|
||||
didLoadChildViews: (List<ViewPB> childViews) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
tabBars: [
|
||||
...state.tabBars,
|
||||
...childViews.map(
|
||||
(newChildView) => TarBar(view: newChildView),
|
||||
),
|
||||
],
|
||||
tabBarControllerByViewId: _extendsTabBarController(childViews),
|
||||
),
|
||||
);
|
||||
},
|
||||
selectView: (String viewId) {
|
||||
final index =
|
||||
state.tabBars.indexWhere((element) => element.viewId == viewId);
|
||||
if (index != -1) {
|
||||
emit(
|
||||
state.copyWith(selectedIndex: index),
|
||||
);
|
||||
}
|
||||
},
|
||||
createView: (action) {
|
||||
_createLinkedView(action.name, action.layoutType);
|
||||
},
|
||||
deleteView: (String viewId) async {
|
||||
final result = await ViewBackendService.delete(viewId: viewId);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
},
|
||||
renameView: (String viewId, String newName) {
|
||||
ViewBackendService.updateView(viewId: viewId, name: newName);
|
||||
},
|
||||
didUpdateChildViews: (updatePB) async {
|
||||
if (updatePB.createChildViews.isNotEmpty) {
|
||||
final allTabBars = [
|
||||
...state.tabBars,
|
||||
...updatePB.createChildViews.map((e) => TarBar(view: e))
|
||||
];
|
||||
emit(
|
||||
state.copyWith(
|
||||
tabBars: allTabBars,
|
||||
selectedIndex: state.tabBars.length,
|
||||
tabBarControllerByViewId:
|
||||
_extendsTabBarController(updatePB.createChildViews),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (updatePB.deleteChildViews.isNotEmpty) {
|
||||
final allTabBars = [...state.tabBars];
|
||||
final tabBarControllerByViewId = {
|
||||
...state.tabBarControllerByViewId
|
||||
};
|
||||
var newSelectedIndex = state.selectedIndex;
|
||||
for (final viewId in updatePB.deleteChildViews) {
|
||||
final index = allTabBars.indexWhere(
|
||||
(element) => element.viewId == viewId,
|
||||
);
|
||||
if (index != -1) {
|
||||
final tarBar = allTabBars.removeAt(index);
|
||||
// Dispose the controller when the tab is removed.
|
||||
final controller =
|
||||
tabBarControllerByViewId.remove(tarBar.viewId);
|
||||
controller?.dispose();
|
||||
}
|
||||
|
||||
if (index == state.selectedIndex) {
|
||||
if (index > 0 && allTabBars.isNotEmpty) {
|
||||
newSelectedIndex = index - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
tabBars: allTabBars,
|
||||
selectedIndex: newSelectedIndex,
|
||||
tabBarControllerByViewId: tabBarControllerByViewId,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
viewDidUpdate: (ViewPB updatedView) {
|
||||
final index = state.tabBars.indexWhere(
|
||||
(element) => element.viewId == updatedView.id,
|
||||
);
|
||||
if (index != -1) {
|
||||
final allTabBars = [...state.tabBars];
|
||||
final updatedTabBar = TarBar(view: updatedView);
|
||||
allTabBars[index] = updatedTabBar;
|
||||
emit(state.copyWith(tabBars: allTabBars));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
for (final tabBar in state.tabBars) {
|
||||
await state.tabBarControllerByViewId[tabBar.viewId]?.dispose();
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _listenInlineViewChanged() {
|
||||
final controller = state.tabBarControllerByViewId[state.parentView.id];
|
||||
controller?.onViewUpdated = (newView) {
|
||||
add(GridTabBarEvent.viewDidUpdate(newView));
|
||||
};
|
||||
|
||||
// Only listen the child view changes when the parent view is inline.
|
||||
controller?.onViewChildViewChanged = (update) {
|
||||
add(GridTabBarEvent.didUpdateChildViews(update));
|
||||
};
|
||||
}
|
||||
|
||||
/// Create tab bar controllers for the new views and return the updated map.
|
||||
Map<String, DatabaseTarBarController> _extendsTabBarController(
|
||||
List<ViewPB> newViews,
|
||||
) {
|
||||
final tabBarControllerByViewId = {...state.tabBarControllerByViewId};
|
||||
for (final view in newViews) {
|
||||
final controller = DatabaseTarBarController(view: view);
|
||||
controller.onViewUpdated = (newView) {
|
||||
add(GridTabBarEvent.viewDidUpdate(newView));
|
||||
};
|
||||
|
||||
tabBarControllerByViewId[view.id] = controller;
|
||||
}
|
||||
return tabBarControllerByViewId;
|
||||
}
|
||||
|
||||
Future<void> _createLinkedView(String name, ViewLayoutPB layoutType) async {
|
||||
final viewId = state.parentView.id;
|
||||
final databaseIdOrError =
|
||||
await DatabaseViewBackendService(viewId: viewId).getDatabaseId();
|
||||
databaseIdOrError.fold(
|
||||
(databaseId) async {
|
||||
final linkedViewOrError =
|
||||
await ViewBackendService.createDatabaseLinkedView(
|
||||
parentViewId: viewId,
|
||||
databaseId: databaseId,
|
||||
layoutType: layoutType,
|
||||
name: name,
|
||||
);
|
||||
|
||||
linkedViewOrError.fold(
|
||||
(linkedView) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadChildView() async {
|
||||
ViewBackendService.getChildViews(viewId: state.parentView.id)
|
||||
.then((viewsOrFail) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
viewsOrFail.fold(
|
||||
(views) => add(GridTabBarEvent.didLoadChildViews(views)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridTabBarEvent with _$GridTabBarEvent {
|
||||
const factory GridTabBarEvent.initial() = _Initial;
|
||||
const factory GridTabBarEvent.didLoadChildViews(
|
||||
List<ViewPB> childViews,
|
||||
) = _DidLoadChildViews;
|
||||
const factory GridTabBarEvent.selectView(String viewId) = _DidSelectView;
|
||||
const factory GridTabBarEvent.createView(AddButtonAction action) =
|
||||
_CreateView;
|
||||
const factory GridTabBarEvent.renameView(String viewId, String newName) =
|
||||
_RenameView;
|
||||
const factory GridTabBarEvent.deleteView(String viewId) = _DeleteView;
|
||||
const factory GridTabBarEvent.didUpdateChildViews(
|
||||
ChildViewUpdatePB updatePB,
|
||||
) = _DidUpdateChildViews;
|
||||
const factory GridTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridTabBarState with _$GridTabBarState {
|
||||
const factory GridTabBarState({
|
||||
required ViewPB parentView,
|
||||
required int selectedIndex,
|
||||
required List<TarBar> tabBars,
|
||||
required Map<String, DatabaseTarBarController> tabBarControllerByViewId,
|
||||
}) = _GridTabBarState;
|
||||
|
||||
factory GridTabBarState.initial(ViewPB view) {
|
||||
final tabBar = TarBar(view: view);
|
||||
return GridTabBarState(
|
||||
parentView: view,
|
||||
selectedIndex: 0,
|
||||
tabBars: [tabBar],
|
||||
tabBarControllerByViewId: {
|
||||
view.id: DatabaseTarBarController(
|
||||
view: view,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TarBar extends Equatable {
|
||||
final ViewPB view;
|
||||
final DatabaseTabBarItemBuilder _builder;
|
||||
|
||||
String get viewId => view.id;
|
||||
DatabaseTabBarItemBuilder get builder => _builder;
|
||||
ViewLayoutPB get layout => view.layout;
|
||||
|
||||
TarBar({
|
||||
required this.view,
|
||||
}) : _builder = view.tarBarItem();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [view.hashCode];
|
||||
}
|
||||
|
||||
typedef OnViewUpdated = void Function(ViewPB newView);
|
||||
typedef OnViewChildViewChanged = void Function(
|
||||
ChildViewUpdatePB childViewUpdate,
|
||||
);
|
||||
|
||||
class DatabaseTarBarController {
|
||||
ViewPB view;
|
||||
final DatabaseController controller;
|
||||
final ViewListener viewListener;
|
||||
OnViewUpdated? onViewUpdated;
|
||||
OnViewChildViewChanged? onViewChildViewChanged;
|
||||
|
||||
DatabaseTarBarController({
|
||||
required this.view,
|
||||
}) : controller = DatabaseController(view: view),
|
||||
viewListener = ViewListener(viewId: view.id) {
|
||||
viewListener.start(
|
||||
onViewChildViewsUpdated: (update) {
|
||||
onViewChildViewChanged?.call(update);
|
||||
},
|
||||
onViewUpdated: (newView) {
|
||||
view = newView;
|
||||
onViewUpdated?.call(newView);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await viewListener.stop();
|
||||
await controller.dispose();
|
||||
}
|
||||
}
|
||||
+29
-19
@@ -35,7 +35,7 @@ class DatabaseViewCache {
|
||||
final String viewId;
|
||||
late RowCache _rowCache;
|
||||
final DatabaseViewListener _databaseViewListener;
|
||||
DatabaseViewCallbacks? _callbacks;
|
||||
final List<DatabaseViewCallbacks> _callbacks = [];
|
||||
|
||||
UnmodifiableListView<RowInfo> get rowInfos => _rowCache.rowInfos;
|
||||
RowCache get rowCache => _rowCache;
|
||||
@@ -61,22 +61,28 @@ class DatabaseViewCache {
|
||||
_rowCache.applyRowsChanged(changeset);
|
||||
|
||||
if (changeset.deletedRows.isNotEmpty) {
|
||||
_callbacks?.onRowsDeleted?.call(changeset.deletedRows);
|
||||
for (final callback in _callbacks) {
|
||||
callback.onRowsDeleted?.call(changeset.deletedRows);
|
||||
}
|
||||
}
|
||||
|
||||
if (changeset.updatedRows.isNotEmpty) {
|
||||
_callbacks?.onRowsUpdated?.call(
|
||||
changeset.updatedRows.map((e) => e.rowId).toList(),
|
||||
_rowCache.changeReason,
|
||||
);
|
||||
for (final callback in _callbacks) {
|
||||
callback.onRowsUpdated?.call(
|
||||
changeset.updatedRows.map((e) => e.rowId).toList(),
|
||||
_rowCache.changeReason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (changeset.insertedRows.isNotEmpty) {
|
||||
_callbacks?.onRowsCreated?.call(
|
||||
changeset.insertedRows
|
||||
.map((insertedRow) => insertedRow.rowMeta.id)
|
||||
.toList(),
|
||||
);
|
||||
for (final callback in _callbacks) {
|
||||
callback.onRowsCreated?.call(
|
||||
changeset.insertedRows
|
||||
.map((insertedRow) => insertedRow.rowMeta.id)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
@@ -103,21 +109,25 @@ class DatabaseViewCache {
|
||||
);
|
||||
|
||||
_rowCache.onRowsChanged(
|
||||
(reason) => _callbacks?.onNumOfRowsChanged?.call(
|
||||
rowInfos,
|
||||
_rowCache.rowByRowId,
|
||||
reason,
|
||||
),
|
||||
(reason) {
|
||||
for (final callback in _callbacks) {
|
||||
callback.onNumOfRowsChanged?.call(
|
||||
rowInfos,
|
||||
_rowCache.rowByRowId,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _databaseViewListener.stop();
|
||||
await _rowCache.dispose();
|
||||
_callbacks = null;
|
||||
_callbacks.clear();
|
||||
}
|
||||
|
||||
void setListener(DatabaseViewCallbacks callbacks) {
|
||||
_callbacks = callbacks;
|
||||
void addListener(DatabaseViewCallbacks callbacks) {
|
||||
_callbacks.add(callbacks);
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -28,11 +28,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
FieldController get fieldController => databaseController.fieldController;
|
||||
String get viewId => databaseController.viewId;
|
||||
|
||||
BoardBloc({required ViewPB view})
|
||||
: databaseController = DatabaseController(
|
||||
view: view,
|
||||
),
|
||||
super(BoardState.initial(view.id)) {
|
||||
BoardBloc({
|
||||
required ViewPB view,
|
||||
required this.databaseController,
|
||||
}) : super(BoardState.initial(view.id)) {
|
||||
boardController = AppFlowyBoardController(
|
||||
onMoveGroup: (
|
||||
fromGroupId,
|
||||
@@ -168,7 +167,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await databaseController.dispose();
|
||||
for (final controller in groupControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
@@ -235,7 +233,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
},
|
||||
);
|
||||
|
||||
databaseController.setListener(
|
||||
databaseController.addListener(
|
||||
onDatabaseChanged: onDatabaseChanged,
|
||||
onGroupChanged: onGroupChanged,
|
||||
);
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'presentation/board_page.dart';
|
||||
|
||||
class BoardPluginBuilder implements PluginBuilder {
|
||||
@override
|
||||
Plugin build(dynamic data) {
|
||||
if (data is ViewPB) {
|
||||
return BoardPlugin(pluginType: pluginType, view: data);
|
||||
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
|
||||
} else {
|
||||
throw FlowyPluginException.invalidData;
|
||||
}
|
||||
@@ -36,55 +31,3 @@ class BoardPluginConfig implements PluginConfig {
|
||||
@override
|
||||
bool get creatable => true;
|
||||
}
|
||||
|
||||
class BoardPlugin extends Plugin {
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
final PluginType _pluginType;
|
||||
|
||||
BoardPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
bool listenOnViewChanged = false,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder =>
|
||||
BoardPluginWidgetBuilder(notifier: notifier);
|
||||
|
||||
@override
|
||||
PluginId get id => notifier.view.id;
|
||||
|
||||
@override
|
||||
PluginType get pluginType => _pluginType;
|
||||
}
|
||||
|
||||
class BoardPluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
final ViewPluginNotifier notifier;
|
||||
BoardPluginWidgetBuilder({required this.notifier, Key? key});
|
||||
|
||||
ViewPB get view => notifier.view;
|
||||
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||
|
||||
@override
|
||||
Widget buildWidget({PluginContext? context}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||
if (deletedView.hasIndex()) {
|
||||
context?.onDeleted(view, deletedView.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return BoardPage(key: ValueKey(view.id), view: view);
|
||||
}
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
}
|
||||
+62
-38
@@ -3,9 +3,12 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
@@ -24,11 +27,48 @@ import '../../widgets/card/card_cell_builder.dart';
|
||||
import '../../widgets/row/cell_builder.dart';
|
||||
import '../application/board_bloc.dart';
|
||||
import '../../widgets/card/card.dart';
|
||||
import 'toolbar/board_toolbar.dart';
|
||||
import 'toolbar/board_setting_bar.dart';
|
||||
|
||||
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return BoardPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) {
|
||||
return BoardSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return SizedBox.fromSize();
|
||||
}
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
return ValueKey(controller.viewId);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardPage extends StatelessWidget {
|
||||
final DatabaseController databaseController;
|
||||
BoardPage({
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
Key? key,
|
||||
this.onEditStateChanged,
|
||||
}) : super(key: ValueKey(view.id));
|
||||
@@ -41,8 +81,10 @@ class BoardPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
BoardBloc(view: view)..add(const BoardEvent.initial()),
|
||||
create: (context) => BoardBloc(
|
||||
view: view,
|
||||
databaseController: databaseController,
|
||||
)..add(const BoardEvent.initial()),
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (p, c) => p.loadingState != c.loadingState,
|
||||
builder: (context, state) {
|
||||
@@ -54,7 +96,7 @@ class BoardPage extends StatelessWidget {
|
||||
(_) => BoardContent(
|
||||
onEditStateChanged: onEditStateChanged,
|
||||
),
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
(err) => FlowyErrorPage.message(err.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -110,14 +152,9 @@ class _BoardContentState extends State<BoardContent> {
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
|
||||
builder: (context, state) {
|
||||
final column = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: column,
|
||||
padding: GridSize.contentInsets,
|
||||
child: _buildBoard(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -125,22 +162,20 @@ class _BoardContentState extends State<BoardContent> {
|
||||
}
|
||||
|
||||
Widget _buildBoard(BuildContext context) {
|
||||
return Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: ScrollController(),
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
return AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: ScrollController(),
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -335,17 +370,6 @@ class _BoardContentState extends State<BoardContent> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ToolbarBlocAdaptor extends StatelessWidget {
|
||||
const _ToolbarBlocAdaptor({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) => const BoardToolbar(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget? _buildHeaderIcon(GroupData customData) {
|
||||
Widget? widget;
|
||||
switch (customData.fieldType) {
|
||||
|
||||
+6
-7
@@ -1,10 +1,11 @@
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class BoardToolbar extends StatelessWidget {
|
||||
const BoardToolbar({
|
||||
class BoardSettingBar extends StatelessWidget {
|
||||
final DatabaseController databaseController;
|
||||
const BoardSettingBar({
|
||||
required this.databaseController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -15,9 +16,7 @@ class BoardToolbar extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
SettingButton(
|
||||
databaseController: context.read<BoardBloc>().databaseController,
|
||||
),
|
||||
SettingButton(databaseController: databaseController),
|
||||
],
|
||||
),
|
||||
);
|
||||
+13
-32
@@ -27,9 +27,8 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
CellCache get cellCache => databaseController.rowCache.cellCache;
|
||||
RowCache get rowCache => databaseController.rowCache;
|
||||
|
||||
CalendarBloc({required ViewPB view})
|
||||
: databaseController = DatabaseController(view: view),
|
||||
super(CalendarState.initial()) {
|
||||
CalendarBloc({required ViewPB view, required this.databaseController})
|
||||
: super(CalendarState.initial()) {
|
||||
on<CalendarEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@@ -39,6 +38,12 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
_loadAllEvents();
|
||||
},
|
||||
didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) {
|
||||
// If the field id changed, reload all events
|
||||
state.settings.fold(() => null, (oldSetting) {
|
||||
if (oldSetting.fieldId != settings.fieldId) {
|
||||
_loadAllEvents();
|
||||
}
|
||||
});
|
||||
emit(state.copyWith(settings: Some(settings)));
|
||||
},
|
||||
didReceiveDatabaseUpdate: (DatabasePB database) {
|
||||
@@ -53,10 +58,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) {
|
||||
_loadAllEvents();
|
||||
emit(state.copyWith(settings: Some(layoutSettings)));
|
||||
},
|
||||
createEvent: (DateTime date, String title) async {
|
||||
await _createEvent(date, title);
|
||||
},
|
||||
@@ -105,12 +106,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await databaseController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
FieldInfo? _getCalendarFieldInfo(String fieldId) {
|
||||
final fieldInfos = databaseController.fieldController.fieldInfos;
|
||||
final index = fieldInfos.indexWhere(
|
||||
@@ -149,7 +144,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
|
||||
Future<void> _createEvent(DateTime date, String title) async {
|
||||
return state.settings.fold(
|
||||
() => null,
|
||||
() {
|
||||
Log.warn('Calendar settings not found');
|
||||
},
|
||||
(settings) async {
|
||||
final dateField = _getCalendarFieldInfo(settings.fieldId);
|
||||
final titleField = _getTitleFieldInfo();
|
||||
@@ -207,7 +204,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
Future<void> _updateCalendarLayoutSetting(
|
||||
CalendarLayoutSettingPB layoutSetting,
|
||||
) async {
|
||||
return databaseController.updateCalenderLayoutSetting(layoutSetting);
|
||||
return databaseController.updateLayoutSetting(layoutSetting);
|
||||
}
|
||||
|
||||
Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {
|
||||
@@ -333,14 +330,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
onLoadLayout: _didReceiveLayoutSetting,
|
||||
);
|
||||
|
||||
final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
|
||||
onCalendarLayoutChanged: _didReceiveNewLayoutField,
|
||||
);
|
||||
|
||||
databaseController.setListener(
|
||||
databaseController.addListener(
|
||||
onDatabaseChanged: onDatabaseChanged,
|
||||
onLayoutChanged: onLayoutChanged,
|
||||
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -353,13 +345,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _didReceiveNewLayoutField(DatabaseLayoutSettingPB layoutSetting) {
|
||||
if (layoutSetting.hasCalendar()) {
|
||||
if (isClosed) return;
|
||||
add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
|
||||
}
|
||||
}
|
||||
|
||||
bool isEventDayChanged(CalendarEventData<CalendarDayEvent> event) {
|
||||
final index = state.allEvents.indexWhere(
|
||||
(element) => element.event!.eventId == event.event!.eventId,
|
||||
@@ -426,10 +411,6 @@ class CalendarEvent with _$CalendarEvent {
|
||||
|
||||
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
|
||||
_ReceiveDatabaseUpdate;
|
||||
|
||||
const factory CalendarEvent.didReceiveNewLayoutField(
|
||||
CalendarLayoutSettingPB layoutSettings,
|
||||
) = _DidReceiveNewLayoutField;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_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:dartz/dartz.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../application/database_controller.dart';
|
||||
import '../../application/row/row_cache.dart';
|
||||
|
||||
part 'unschedule_event_bloc.freezed.dart';
|
||||
|
||||
class UnscheduleEventsBloc
|
||||
extends Bloc<UnscheduleEventsEvent, UnscheduleEventsState> {
|
||||
final DatabaseController databaseController;
|
||||
Map<String, FieldInfo> fieldInfoByFieldId = {};
|
||||
|
||||
// Getters
|
||||
String get viewId => databaseController.viewId;
|
||||
FieldController get fieldController => databaseController.fieldController;
|
||||
CellCache get cellCache => databaseController.rowCache.cellCache;
|
||||
RowCache get rowCache => databaseController.rowCache;
|
||||
|
||||
UnscheduleEventsBloc({
|
||||
required this.databaseController,
|
||||
}) : super(UnscheduleEventsState.initial()) {
|
||||
on<UnscheduleEventsEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
_loadAllEvents();
|
||||
},
|
||||
didLoadAllEvents: (events) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allEvents: events,
|
||||
unscheduleEvents:
|
||||
events.where((element) => !element.isScheduled).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
didDeleteEvents: (List<RowId> deletedRowIds) {
|
||||
final events = [...state.allEvents];
|
||||
events.retainWhere(
|
||||
(element) => !deletedRowIds.contains(element.rowMeta.id),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
allEvents: events,
|
||||
unscheduleEvents:
|
||||
events.where((element) => !element.isScheduled).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveEvent: (CalendarEventPB event) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allEvents: [...state.allEvents, event],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<CalendarEventPB?> _loadEvent(
|
||||
RowId rowId,
|
||||
) async {
|
||||
final payload = RowIdPB(viewId: viewId, rowId: rowId);
|
||||
return DatabaseEventGetCalendarEvent(payload).send().then(
|
||||
(result) => result.fold(
|
||||
(eventPB) => eventPB,
|
||||
(r) {
|
||||
Log.error(r);
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadAllEvents() async {
|
||||
final payload = CalendarEventRequestPB.create()..viewId = viewId;
|
||||
DatabaseEventGetAllCalendarEvents(payload).send().then((result) {
|
||||
result.fold(
|
||||
(events) {
|
||||
if (!isClosed) {
|
||||
add(UnscheduleEventsEvent.didLoadAllEvents(events.items));
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
final onDatabaseChanged = DatabaseCallbacks(
|
||||
onRowsCreated: (rowIds) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
for (final id in rowIds) {
|
||||
final event = await _loadEvent(id);
|
||||
if (event != null && !isClosed) {
|
||||
add(UnscheduleEventsEvent.didReceiveEvent(event));
|
||||
}
|
||||
}
|
||||
},
|
||||
onRowsDeleted: (rowIds) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
add(UnscheduleEventsEvent.didDeleteEvents(rowIds));
|
||||
},
|
||||
onRowsUpdated: (rowIds, reason) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
for (final id in rowIds) {
|
||||
final event = await _loadEvent(id);
|
||||
if (event != null) {
|
||||
add(UnscheduleEventsEvent.didDeleteEvents([id]));
|
||||
add(UnscheduleEventsEvent.didReceiveEvent(event));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class UnscheduleEventsEvent with _$UnscheduleEventsEvent {
|
||||
const factory UnscheduleEventsEvent.initial() = _InitialCalendar;
|
||||
|
||||
// Called after loading all the current evnets
|
||||
const factory UnscheduleEventsEvent.didLoadAllEvents(
|
||||
List<CalendarEventPB> events,
|
||||
) = _ReceiveUnscheduleEventsEvents;
|
||||
|
||||
const factory UnscheduleEventsEvent.didDeleteEvents(List<RowId> rowIds) =
|
||||
_DidDeleteEvents;
|
||||
|
||||
const factory UnscheduleEventsEvent.didReceiveEvent(
|
||||
CalendarEventPB event,
|
||||
) = _DidReceiveEvent;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class UnscheduleEventsState with _$UnscheduleEventsState {
|
||||
const factory UnscheduleEventsState({
|
||||
required Option<DatabasePB> database,
|
||||
required List<CalendarEventPB> allEvents,
|
||||
required List<CalendarEventPB> unscheduleEvents,
|
||||
}) = _UnscheduleEventsState;
|
||||
|
||||
factory UnscheduleEventsState.initial() => UnscheduleEventsState(
|
||||
database: none(),
|
||||
allEvents: [],
|
||||
unscheduleEvents: [],
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../util.dart';
|
||||
import 'presentation/calendar_page.dart';
|
||||
|
||||
class CalendarPluginBuilder extends PluginBuilder {
|
||||
@override
|
||||
Plugin build(dynamic data) {
|
||||
if (data is ViewPB) {
|
||||
return CalendarPlugin(pluginType: pluginType, view: data);
|
||||
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
|
||||
} else {
|
||||
throw FlowyPluginException.invalidData;
|
||||
}
|
||||
@@ -36,55 +31,3 @@ class CalendarPluginConfig implements PluginConfig {
|
||||
@override
|
||||
bool get creatable => true;
|
||||
}
|
||||
|
||||
class CalendarPlugin extends Plugin {
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
final PluginType _pluginType;
|
||||
|
||||
CalendarPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
bool listenOnViewChanged = false,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder =>
|
||||
CalendarPluginWidgetBuilder(notifier: notifier);
|
||||
|
||||
@override
|
||||
PluginId get id => notifier.view.id;
|
||||
|
||||
@override
|
||||
PluginType get pluginType => _pluginType;
|
||||
}
|
||||
|
||||
class CalendarPluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
final ViewPluginNotifier notifier;
|
||||
CalendarPluginWidgetBuilder({required this.notifier, Key? key});
|
||||
|
||||
ViewPB get view => notifier.view;
|
||||
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||
|
||||
@override
|
||||
Widget buildWidget({PluginContext? context}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||
if (deletedView.hasIndex()) {
|
||||
context?.onDeleted(view, deletedView.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return CalendarPage(key: ValueKey(view.id), view: view);
|
||||
}
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
}
|
||||
+1
-1
@@ -309,7 +309,7 @@ class _EventCard extends StatelessWidget {
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) => showEventDetails(
|
||||
context: context,
|
||||
event: event,
|
||||
event: event.event,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
|
||||
+65
-18
@@ -1,5 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -15,11 +19,51 @@ import '../../widgets/row/cell_builder.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_toolbar.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
|
||||
class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return CalendarPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) {
|
||||
return CalendarSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return SizedBox.fromSize();
|
||||
}
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
return ValueKey(controller.viewId);
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarPage extends StatefulWidget {
|
||||
final ViewPB view;
|
||||
const CalendarPage({required this.view, super.key});
|
||||
final DatabaseController databaseController;
|
||||
const CalendarPage({
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CalendarPage> createState() => _CalendarPageState();
|
||||
@@ -33,8 +77,10 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
@override
|
||||
void initState() {
|
||||
_calendarState = GlobalKey<MonthViewState>();
|
||||
_calendarBloc = CalendarBloc(view: widget.view)
|
||||
..add(const CalendarEvent.initial());
|
||||
_calendarBloc = CalendarBloc(
|
||||
view: widget.view,
|
||||
databaseController: widget.databaseController,
|
||||
)..add(const CalendarEvent.initial());
|
||||
|
||||
super.initState();
|
||||
}
|
||||
@@ -79,7 +125,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
if (state.editingEvent != null) {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: state.editingEvent!.event!,
|
||||
event: state.editingEvent!.event!.event,
|
||||
viewId: widget.view.id,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
);
|
||||
@@ -115,8 +161,6 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
// const _ToolbarBlocAdaptor(),
|
||||
const CalendarToolbar(),
|
||||
_buildCalendar(
|
||||
_eventController,
|
||||
state.settings
|
||||
@@ -133,15 +177,18 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
Widget _buildCalendar(EventController eventController, int firstDayOfWeek) {
|
||||
return Expanded(
|
||||
child: MonthView(
|
||||
key: _calendarState,
|
||||
controller: _eventController,
|
||||
cellAspectRatio: .6,
|
||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||
borderColor: Theme.of(context).dividerColor,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
weekDayBuilder: _headerWeekDayBuilder,
|
||||
cellBuilder: _calendarDayBuilder,
|
||||
child: Padding(
|
||||
padding: GridSize.contentInsets,
|
||||
child: MonthView(
|
||||
key: _calendarState,
|
||||
controller: _eventController,
|
||||
cellAspectRatio: .6,
|
||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||
borderColor: Theme.of(context).dividerColor,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
weekDayBuilder: _headerWeekDayBuilder,
|
||||
cellBuilder: _calendarDayBuilder,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -238,12 +285,12 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
void showEventDetails({
|
||||
required BuildContext context,
|
||||
required CalendarDayEvent event,
|
||||
required CalendarEventPB event,
|
||||
required String viewId,
|
||||
required RowCache rowCache,
|
||||
}) {
|
||||
final dataController = RowController(
|
||||
rowMeta: event.event.rowMeta,
|
||||
rowMeta: event.rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
+37
-16
@@ -351,21 +351,16 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
final symbols =
|
||||
DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
|
||||
// starts from sunday
|
||||
final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final string = entry.value;
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(string),
|
||||
onTap: () {
|
||||
onUpdated(index);
|
||||
popoverMutex.close();
|
||||
},
|
||||
rightIcon: firstDayOfWeek == index
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
),
|
||||
const len = 2;
|
||||
final items = symbols.WEEKDAYS.take(len).indexed.map((entry) {
|
||||
return StartFromButton(
|
||||
title: entry.$2,
|
||||
dayIndex: entry.$1,
|
||||
isSelected: firstDayOfWeek == entry.$1,
|
||||
onTap: (index) {
|
||||
onUpdated(index);
|
||||
popoverMutex.close();
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@@ -376,7 +371,7 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
itemBuilder: (context, index) => items[index],
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
itemCount: 2,
|
||||
itemCount: len,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -426,3 +421,29 @@ enum CalendarLayoutSettingAction {
|
||||
showWeekNumber,
|
||||
showTimeLine,
|
||||
}
|
||||
|
||||
class StartFromButton extends StatelessWidget {
|
||||
final int dayIndex;
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
final void Function(int) onTap;
|
||||
const StartFromButton({
|
||||
required this.title,
|
||||
required this.dayIndex,
|
||||
required this.onTap,
|
||||
required this.isSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(title),
|
||||
onTap: () => onTap(dayIndex),
|
||||
rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CalendarSettingBar extends StatelessWidget {
|
||||
final DatabaseController databaseController;
|
||||
const CalendarSettingBar({
|
||||
required this.databaseController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
UnscheduleEventsButton(databaseController: databaseController),
|
||||
SettingButton(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduleEventsButton extends StatefulWidget {
|
||||
final DatabaseController databaseController;
|
||||
const UnscheduleEventsButton({
|
||||
required this.databaseController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UnscheduleEventsButton> createState() => _UnscheduleEventsButtonState();
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
|
||||
late final PopoverController _popoverController;
|
||||
late final UnscheduleEventsBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = UnscheduleEventsBloc(databaseController: widget.databaseController)
|
||||
..add(const UnscheduleEventsEvent.initial());
|
||||
_popoverController = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 8),
|
||||
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
|
||||
child: BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.unscheduleEvents.length !=
|
||||
current.unscheduleEvents.length,
|
||||
builder: (context, state) {
|
||||
return FlowyTextButton(
|
||||
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
return UnscheduleEventsList(
|
||||
viewId: _bloc.viewId,
|
||||
rowCache: _bloc.rowCache,
|
||||
controller: _popoverController,
|
||||
unscheduleEvents: _bloc.state.unscheduleEvents,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduleEventsList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final RowCache rowCache;
|
||||
final PopoverController controller;
|
||||
final List<CalendarEventPB> unscheduleEvents;
|
||||
const UnscheduleEventsList({
|
||||
required this.viewId,
|
||||
required this.controller,
|
||||
required this.unscheduleEvents,
|
||||
required this.rowCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.calendar_settings_clickToAdd.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
...unscheduleEvents.map(
|
||||
(e) => UnscheduledEventCell(
|
||||
event: e,
|
||||
onPressed: () {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: e,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduledEventCell extends StatelessWidget {
|
||||
final CalendarEventPB event;
|
||||
final VoidCallback onPressed;
|
||||
const UnscheduledEventCell({
|
||||
required this.event,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
event.title.isEmpty
|
||||
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
|
||||
: event.title,
|
||||
),
|
||||
onTap: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../application/calendar_bloc.dart';
|
||||
|
||||
class CalendarToolbar extends StatelessWidget {
|
||||
const CalendarToolbar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const _UnscheduleEventsButton(),
|
||||
SettingButton(
|
||||
databaseController: context.read<CalendarBloc>().databaseController,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButton extends StatefulWidget {
|
||||
const _UnscheduleEventsButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_UnscheduleEventsButton> createState() =>
|
||||
_UnscheduleEventsButtonState();
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
|
||||
late final PopoverController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CalendarBloc, CalendarState>(
|
||||
builder: (context, state) {
|
||||
final unscheduledEvents = state.allEvents
|
||||
.where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0))
|
||||
.toList();
|
||||
final viewId = context.read<CalendarBloc>().viewId;
|
||||
final rowCache = context.read<CalendarBloc>().rowCache;
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
controller: _controller,
|
||||
offset: const Offset(0, 8),
|
||||
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
|
||||
child: FlowyTextButton(
|
||||
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
final cells = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: FlowyText.medium(
|
||||
// LocaleKeys.calendar_settings_noDateHint.tr(),
|
||||
LocaleKeys.calendar_settings_clickToAdd.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
...unscheduledEvents.map(
|
||||
(e) => _UnscheduledEventItem(
|
||||
event: e,
|
||||
onPressed: () {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: e.event!,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
_controller.close();
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnscheduledEventItem extends StatelessWidget {
|
||||
final CalendarEventData<CalendarDayEvent> event;
|
||||
final VoidCallback onPressed;
|
||||
const _UnscheduledEventItem({
|
||||
required this.event,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
event.title.isEmpty
|
||||
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
|
||||
: event.title,
|
||||
),
|
||||
onTap: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -3,17 +3,17 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'grid_accessory_bloc.freezed.dart';
|
||||
|
||||
class GridAccessoryMenuBloc
|
||||
extends Bloc<GridAccessoryMenuEvent, GridAccessoryMenuState> {
|
||||
class DatabaseViewSettingExtensionBloc extends Bloc<
|
||||
DatabaseViewSettingExtensionEvent, DatabaseViewSettingExtensionState> {
|
||||
final String viewId;
|
||||
|
||||
GridAccessoryMenuBloc({required this.viewId})
|
||||
DatabaseViewSettingExtensionBloc({required this.viewId})
|
||||
: super(
|
||||
GridAccessoryMenuState.initial(
|
||||
DatabaseViewSettingExtensionState.initial(
|
||||
viewId,
|
||||
),
|
||||
) {
|
||||
on<GridAccessoryMenuEvent>(
|
||||
on<DatabaseViewSettingExtensionEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {},
|
||||
@@ -27,22 +27,25 @@ class GridAccessoryMenuBloc
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridAccessoryMenuEvent with _$GridAccessoryMenuEvent {
|
||||
const factory GridAccessoryMenuEvent.initial() = _Initial;
|
||||
const factory GridAccessoryMenuEvent.toggleMenu() = _MenuVisibleChange;
|
||||
class DatabaseViewSettingExtensionEvent
|
||||
with _$DatabaseViewSettingExtensionEvent {
|
||||
const factory DatabaseViewSettingExtensionEvent.initial() = _Initial;
|
||||
const factory DatabaseViewSettingExtensionEvent.toggleMenu() =
|
||||
_MenuVisibleChange;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridAccessoryMenuState with _$GridAccessoryMenuState {
|
||||
const factory GridAccessoryMenuState({
|
||||
class DatabaseViewSettingExtensionState
|
||||
with _$DatabaseViewSettingExtensionState {
|
||||
const factory DatabaseViewSettingExtensionState({
|
||||
required String viewId,
|
||||
required bool isVisible,
|
||||
}) = _GridAccessoryMenuState;
|
||||
}) = _DatabaseViewSettingExtensionState;
|
||||
|
||||
factory GridAccessoryMenuState.initial(
|
||||
factory DatabaseViewSettingExtensionState.initial(
|
||||
String viewId,
|
||||
) =>
|
||||
GridAccessoryMenuState(
|
||||
DatabaseViewSettingExtensionState(
|
||||
viewId: viewId,
|
||||
isVisible: false,
|
||||
);
|
||||
|
||||
+45
-10
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@@ -65,17 +67,27 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceveFilters: (List<FilterInfo> filters) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
reorderable: filters.isEmpty && state.sorts.isEmpty,
|
||||
filters: filters,
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceveSorts: (List<SortInfo> sorts) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
reorderable: sorts.isEmpty && state.filters.isEmpty,
|
||||
sorts: sorts,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await databaseController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
RowCache getRowCache(RowId rowId) {
|
||||
return databaseController.rowCache;
|
||||
}
|
||||
@@ -93,17 +105,29 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
}
|
||||
},
|
||||
onRowsUpdated: (rows, reason) {
|
||||
add(
|
||||
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
|
||||
);
|
||||
if (!isClosed) {
|
||||
add(
|
||||
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFieldsChanged: (fields) {
|
||||
if (!isClosed) {
|
||||
add(GridEvent.didReceiveFieldUpdate(fields));
|
||||
}
|
||||
},
|
||||
onFiltersChanged: (filters) {
|
||||
if (!isClosed) {
|
||||
add(GridEvent.didReceveFilters(filters));
|
||||
}
|
||||
},
|
||||
onSortsChanged: (sorts) {
|
||||
if (!isClosed) {
|
||||
add(GridEvent.didReceveSorts(sorts));
|
||||
}
|
||||
},
|
||||
);
|
||||
databaseController.setListener(onDatabaseChanged: onDatabaseChanged);
|
||||
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
|
||||
}
|
||||
|
||||
Future<void> _openGrid(Emitter<GridState> emit) async {
|
||||
@@ -138,6 +162,11 @@ class GridEvent with _$GridEvent {
|
||||
const factory GridEvent.didReceiveGridUpdate(
|
||||
DatabasePB grid,
|
||||
) = _DidReceiveGridUpdate;
|
||||
|
||||
const factory GridEvent.didReceveFilters(List<FilterInfo> filters) =
|
||||
_DidReceiveFilters;
|
||||
const factory GridEvent.didReceveSorts(List<SortInfo> sorts) =
|
||||
_DidReceiveSorts;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -149,7 +178,10 @@ class GridState with _$GridState {
|
||||
required List<RowInfo> rowInfos,
|
||||
required int rowCount,
|
||||
required GridLoadingState loadingState,
|
||||
required bool reorderable,
|
||||
required RowsChangedReason reason,
|
||||
required List<SortInfo> sorts,
|
||||
required List<FilterInfo> filters,
|
||||
}) = _GridState;
|
||||
|
||||
factory GridState.initial(String viewId) => GridState(
|
||||
@@ -158,8 +190,11 @@ class GridState with _$GridState {
|
||||
rowCount: 0,
|
||||
grid: none(),
|
||||
viewId: viewId,
|
||||
reorderable: true,
|
||||
loadingState: const _Loading(),
|
||||
reason: const InitialListState(),
|
||||
filters: [],
|
||||
sorts: [],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'presentation/grid_page.dart';
|
||||
|
||||
class GridPluginBuilder implements PluginBuilder {
|
||||
@override
|
||||
Plugin build(dynamic data) {
|
||||
if (data is ViewPB) {
|
||||
return GridPlugin(pluginType: pluginType, view: data);
|
||||
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
|
||||
} else {
|
||||
throw FlowyPluginException.invalidData;
|
||||
}
|
||||
@@ -36,55 +31,3 @@ class GridPluginConfig implements PluginConfig {
|
||||
@override
|
||||
bool get creatable => true;
|
||||
}
|
||||
|
||||
class GridPlugin extends Plugin {
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
final PluginType _pluginType;
|
||||
|
||||
GridPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
bool listenOnViewChanged = false,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder =>
|
||||
GridPluginWidgetBuilder(notifier: notifier);
|
||||
|
||||
@override
|
||||
PluginId get id => notifier.view.id;
|
||||
|
||||
@override
|
||||
PluginType get pluginType => _pluginType;
|
||||
}
|
||||
|
||||
class GridPluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
final ViewPluginNotifier notifier;
|
||||
ViewPB get view => notifier.view;
|
||||
|
||||
GridPluginWidgetBuilder({required this.notifier, Key? key});
|
||||
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||
|
||||
@override
|
||||
Widget buildWidget({PluginContext? context}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||
if (deletedView.hasIndex()) {
|
||||
context?.onDeleted(view, deletedView.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return GridPage(key: ValueKey(view.id), view: view);
|
||||
}
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
}
|
||||
+167
-133
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -15,32 +17,82 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||
import '../../application/field/field_controller.dart';
|
||||
import '../../application/row/row_cache.dart';
|
||||
import '../../application/row/row_data_controller.dart';
|
||||
import '../../application/setting/setting_bloc.dart';
|
||||
import '../application/filter/filter_menu_bloc.dart';
|
||||
import '../application/grid_bloc.dart';
|
||||
import '../../application/database_controller.dart';
|
||||
import '../application/sort/sort_menu_bloc.dart';
|
||||
import 'grid_scroll.dart';
|
||||
import '../../tar_bar/tab_bar_view.dart';
|
||||
import 'layout/layout.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'widgets/accessory_menu.dart';
|
||||
import 'widgets/row/row.dart';
|
||||
import 'widgets/footer/grid_footer.dart';
|
||||
import 'widgets/header/grid_header.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
import 'widgets/shortcuts.dart';
|
||||
import 'widgets/toolbar/grid_toolbar.dart';
|
||||
|
||||
class ToggleExtensionNotifier extends ChangeNotifier {
|
||||
bool _isToggled = false;
|
||||
|
||||
get isToggled => _isToggled;
|
||||
|
||||
void toggle() {
|
||||
_isToggled = !_isToggled;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
final _toggleExtension = ToggleExtensionNotifier();
|
||||
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return GridPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) {
|
||||
return GridSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
controller: controller,
|
||||
toggleExtension: _toggleExtension,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return DatabaseViewSettingExtension(
|
||||
key: _makeValueKey(controller),
|
||||
viewId: controller.viewId,
|
||||
databaseController: controller,
|
||||
toggleExtension: _toggleExtension,
|
||||
);
|
||||
}
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
return ValueKey(controller.viewId);
|
||||
}
|
||||
}
|
||||
|
||||
class GridPage extends StatefulWidget {
|
||||
GridPage({
|
||||
final DatabaseController databaseController;
|
||||
const GridPage({
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
this.onDeleted,
|
||||
Key? key,
|
||||
}) : databaseController = DatabaseController(view: view),
|
||||
super(key: key);
|
||||
}) : super(key: key);
|
||||
|
||||
final ViewPB view;
|
||||
final DatabaseController databaseController;
|
||||
final VoidCallback? onDeleted;
|
||||
|
||||
@override
|
||||
@@ -48,6 +100,11 @@ class GridPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridPageState extends State<GridPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@@ -58,21 +115,6 @@ class _GridPageState extends State<GridPage> {
|
||||
databaseController: widget.databaseController,
|
||||
)..add(const GridEvent.initial()),
|
||||
),
|
||||
BlocProvider<GridFilterMenuBloc>(
|
||||
create: (context) => GridFilterMenuBloc(
|
||||
viewId: widget.view.id,
|
||||
fieldController: widget.databaseController.fieldController,
|
||||
)..add(const GridFilterMenuEvent.initial()),
|
||||
),
|
||||
BlocProvider<SortMenuBloc>(
|
||||
create: (context) => SortMenuBloc(
|
||||
viewId: widget.view.id,
|
||||
fieldController: widget.databaseController.fieldController,
|
||||
)..add(const SortMenuEvent.initial()),
|
||||
),
|
||||
BlocProvider<DatabaseSettingBloc>(
|
||||
create: (context) => DatabaseSettingBloc(viewId: widget.view.id),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<GridBloc, GridState>(
|
||||
builder: (context, state) {
|
||||
@@ -81,11 +123,12 @@ class _GridPageState extends State<GridPage> {
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => GridShortcuts(
|
||||
child: FlowyGrid(
|
||||
viewId: widget.view.id,
|
||||
),
|
||||
child: GridPageContent(view: widget.view),
|
||||
),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -94,18 +137,18 @@ class _GridPageState extends State<GridPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class FlowyGrid extends StatefulWidget {
|
||||
final String viewId;
|
||||
const FlowyGrid({
|
||||
required this.viewId,
|
||||
class GridPageContent extends StatefulWidget {
|
||||
final ViewPB view;
|
||||
const GridPageContent({
|
||||
required this.view,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FlowyGrid> createState() => _FlowyGridState();
|
||||
State<GridPageContent> createState() => _GridPageContentState();
|
||||
}
|
||||
|
||||
class _FlowyGridState extends State<FlowyGrid> {
|
||||
class _GridPageContentState extends State<GridPageContent> {
|
||||
final _scrollController = GridScrollController(
|
||||
scrollGroupController: LinkedScrollControllerGroup(),
|
||||
);
|
||||
@@ -129,106 +172,114 @@ class _FlowyGridState extends State<FlowyGrid> {
|
||||
buildWhen: (previous, current) => previous.fields != current.fields,
|
||||
builder: (context, state) {
|
||||
final contentWidth = GridLayout.headerWidth(state.fields.value);
|
||||
final child = _WrapScrollView(
|
||||
scrollController: _scrollController,
|
||||
contentWidth: contentWidth,
|
||||
child: _GridRows(
|
||||
viewId: widget.viewId,
|
||||
verticalScrollController: _scrollController.verticalController,
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const GridToolbar(),
|
||||
GridAccessoryMenu(viewId: state.viewId),
|
||||
_gridHeader(context, state.viewId),
|
||||
Flexible(child: child),
|
||||
const _RowCountBadge(),
|
||||
_GridHeader(headerScrollController: headerScrollController),
|
||||
_GridRows(
|
||||
viewId: state.viewId,
|
||||
contentWidth: contentWidth,
|
||||
scrollController: _scrollController,
|
||||
),
|
||||
const _GridFooter(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _gridHeader(BuildContext context, String viewId) {
|
||||
final fieldController =
|
||||
context.read<GridBloc>().databaseController.fieldController;
|
||||
return GridHeaderSliverAdaptor(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
anchorScrollController: headerScrollController,
|
||||
class _GridHeader extends StatelessWidget {
|
||||
final ScrollController headerScrollController;
|
||||
const _GridHeader({required this.headerScrollController});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridBloc, GridState>(
|
||||
builder: (context, state) {
|
||||
return GridHeaderSliverAdaptor(
|
||||
viewId: state.viewId,
|
||||
fieldController:
|
||||
context.read<GridBloc>().databaseController.fieldController,
|
||||
anchorScrollController: headerScrollController,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridRows extends StatelessWidget {
|
||||
final String viewId;
|
||||
final double contentWidth;
|
||||
final GridScrollController scrollController;
|
||||
|
||||
const _GridRows({
|
||||
required this.viewId,
|
||||
required this.verticalScrollController,
|
||||
required this.contentWidth,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
final ScrollController verticalScrollController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final filterState = context.watch<GridFilterMenuBloc>().state;
|
||||
final sortState = context.watch<SortMenuBloc>().state;
|
||||
|
||||
return BlocBuilder<GridBloc, GridState>(
|
||||
buildWhen: (previous, current) => current.reason.maybeWhen(
|
||||
reorderRows: () => true,
|
||||
reorderSingleRow: (reorderRow, rowInfo) => true,
|
||||
delete: (item) => true,
|
||||
insert: (item) => true,
|
||||
orElse: () => false,
|
||||
),
|
||||
builder: (context, state) {
|
||||
final rowInfos = state.rowInfos;
|
||||
final behavior = ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: false,
|
||||
);
|
||||
return ScrollConfiguration(
|
||||
behavior: behavior,
|
||||
child: ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: verticalScrollController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<GridBloc>()
|
||||
.add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
index: index,
|
||||
isSortEnabled: sortState.sortInfos.isNotEmpty,
|
||||
isFilterEnabled: filterState.filters.isNotEmpty,
|
||||
);
|
||||
}
|
||||
return const _GridFooter(key: Key('gridFooter'));
|
||||
},
|
||||
return Flexible(
|
||||
child: _WrapScrollView(
|
||||
scrollController: scrollController,
|
||||
contentWidth: contentWidth,
|
||||
child: BlocBuilder<GridBloc, GridState>(
|
||||
buildWhen: (previous, current) => current.reason.maybeWhen(
|
||||
reorderRows: () => true,
|
||||
reorderSingleRow: (reorderRow, rowInfo) => true,
|
||||
delete: (item) => true,
|
||||
insert: (item) => true,
|
||||
orElse: () => false,
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (context, state) {
|
||||
final rowInfos = state.rowInfos;
|
||||
final behavior = ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: false,
|
||||
);
|
||||
return ScrollConfiguration(
|
||||
behavior: behavior,
|
||||
child: ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex =
|
||||
newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<GridBloc>()
|
||||
.add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -236,8 +287,7 @@ class _GridRows extends StatelessWidget {
|
||||
BuildContext context,
|
||||
RowId rowId, {
|
||||
int? index,
|
||||
bool isSortEnabled = false,
|
||||
bool isFilterEnabled = false,
|
||||
required bool isDraggable,
|
||||
Animation<double>? animation,
|
||||
}) {
|
||||
final rowCache = context.read<GridBloc>().getRowCache(rowId);
|
||||
@@ -259,7 +309,7 @@ class _GridRows extends StatelessWidget {
|
||||
rowId: rowId,
|
||||
viewId: viewId,
|
||||
index: index,
|
||||
isDraggable: !isSortEnabled && !isFilterEnabled,
|
||||
isDraggable: isDraggable,
|
||||
dataController: dataController,
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
openDetailPage: (context, cellBuilder) {
|
||||
@@ -314,22 +364,6 @@ class _GridRows extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _GridFooter extends StatelessWidget {
|
||||
const _GridFooter({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: GridSize.footerContentInsets,
|
||||
height: GridSize.footerHeight,
|
||||
margin: const EdgeInsets.only(bottom: 200),
|
||||
child: const GridAddRowButton(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WrapScrollView extends StatelessWidget {
|
||||
const _WrapScrollView({
|
||||
required this.contentWidth,
|
||||
@@ -360,8 +394,8 @@ class _WrapScrollView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RowCountBadge extends StatelessWidget {
|
||||
const _RowCountBadge();
|
||||
class _GridFooter extends StatelessWidget {
|
||||
const _GridFooter();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -369,7 +403,7 @@ class _RowCountBadge extends StatelessWidget {
|
||||
selector: (state) => state.rowCount,
|
||||
builder: (context, rowCount) {
|
||||
return Padding(
|
||||
padding: GridSize.footerContentInsets,
|
||||
padding: GridSize.contentInsets,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
+7
@@ -36,4 +36,11 @@ class GridSize {
|
||||
GridSize.headerContainerPadding,
|
||||
GridSize.headerContainerPadding,
|
||||
);
|
||||
|
||||
static EdgeInsets get contentInsets => EdgeInsets.fromLTRB(
|
||||
GridSize.leadingHeaderPadding,
|
||||
GridSize.headerContainerPadding,
|
||||
GridSize.leadingHeaderPadding,
|
||||
GridSize.headerContainerPadding,
|
||||
);
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../layout/sizes.dart';
|
||||
import 'filter/filter_menu.dart';
|
||||
import 'sort/sort_menu.dart';
|
||||
|
||||
class GridAccessoryMenu extends StatelessWidget {
|
||||
final String viewId;
|
||||
const GridAccessoryMenu({required this.viewId, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => GridAccessoryMenuBloc(viewId: viewId),
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
listenWhen: (p, c) => p.isVisible != c.isVisible,
|
||||
listener: (context, state) => context
|
||||
.read<GridAccessoryMenuBloc>()
|
||||
.add(const GridAccessoryMenuEvent.toggleMenu()),
|
||||
),
|
||||
BlocListener<SortMenuBloc, SortMenuState>(
|
||||
listenWhen: (p, c) => p.isVisible != c.isVisible,
|
||||
listener: (context, state) => context
|
||||
.read<GridAccessoryMenuBloc>()
|
||||
.add(const GridAccessoryMenuEvent.toggleMenu()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
|
||||
builder: (context, state) {
|
||||
if (state.isVisible) {
|
||||
return const _AccessoryMenu();
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccessoryMenu extends StatelessWidget {
|
||||
const _AccessoryMenu({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
|
||||
builder: (context, state) {
|
||||
return _wrapPadding(
|
||||
Column(
|
||||
children: [
|
||||
Divider(
|
||||
height: 1.0,
|
||||
color: AFThemeExtension.of(context).toggleOffFill,
|
||||
),
|
||||
const VSpace(6),
|
||||
const IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
SortMenu(),
|
||||
HSpace(6),
|
||||
FilterMenu(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapPadding(Widget child) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.leadingHeaderPadding,
|
||||
vertical: 6,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -109,7 +109,7 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
|
||||
children: [
|
||||
FlowyText(state.filterInfo.fieldInfo.name),
|
||||
const HSpace(4),
|
||||
CheckboxFilterConditionPBList(
|
||||
CheckboxFilterConditionList(
|
||||
filterInfo: state.filterInfo,
|
||||
popoverMutex: popoverMutex,
|
||||
onCondition: (condition) {
|
||||
@@ -137,11 +137,11 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxFilterConditionPBList extends StatelessWidget {
|
||||
class CheckboxFilterConditionList extends StatelessWidget {
|
||||
final FilterInfo filterInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
final Function(CheckboxFilterConditionPB) onCondition;
|
||||
const CheckboxFilterConditionPBList({
|
||||
const CheckboxFilterConditionList({
|
||||
required this.filterInfo,
|
||||
required this.popoverMutex,
|
||||
required this.onCondition,
|
||||
|
||||
+3
-3
@@ -93,7 +93,7 @@ class ChecklistState extends State<ChecklistFilterEditor> {
|
||||
children: [
|
||||
FlowyText(state.filterInfo.fieldInfo.name),
|
||||
const HSpace(4),
|
||||
ChecklistFilterConditionPBList(
|
||||
ChecklistFilterConditionList(
|
||||
filterInfo: state.filterInfo,
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -118,9 +118,9 @@ class ChecklistState extends State<ChecklistFilterEditor> {
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistFilterConditionPBList extends StatelessWidget {
|
||||
class ChecklistFilterConditionList extends StatelessWidget {
|
||||
final FilterInfo filterInfo;
|
||||
const ChecklistFilterConditionPBList({
|
||||
const ChecklistFilterConditionList({
|
||||
required this.filterInfo,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
+6
-8
@@ -2,7 +2,6 @@ import 'package:appflowy/plugins/database_view/grid/application/filter/select_op
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -58,16 +57,16 @@ class SelectOptionFilterList extends StatelessWidget {
|
||||
SelectOptionFilterListState>(
|
||||
builder: (context, state) {
|
||||
return ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
controller: ScrollController(),
|
||||
itemCount: state.visibleOptions.length,
|
||||
separatorBuilder: (context, index) {
|
||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||
},
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final option = state.visibleOptions[index];
|
||||
return _SelectOptionFilterCell(
|
||||
return SelectOptionFilterCell(
|
||||
option: option.optionPB,
|
||||
isSelected: option.isSelected,
|
||||
);
|
||||
@@ -80,21 +79,20 @@ class SelectOptionFilterList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectOptionFilterCell extends StatefulWidget {
|
||||
class SelectOptionFilterCell extends StatefulWidget {
|
||||
final SelectOptionPB option;
|
||||
final bool isSelected;
|
||||
const _SelectOptionFilterCell({
|
||||
const SelectOptionFilterCell({
|
||||
required this.option,
|
||||
required this.isSelected,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_SelectOptionFilterCell> createState() =>
|
||||
_SelectOptionFilterCellState();
|
||||
State<SelectOptionFilterCell> createState() => _SelectOptionFilterCellState();
|
||||
}
|
||||
|
||||
class _SelectOptionFilterCellState extends State<_SelectOptionFilterCell> {
|
||||
class _SelectOptionFilterCellState extends State<SelectOptionFilterCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
|
||||
final cells = state.creatableFields.map((fieldInfo) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: _FilterPropertyCell(
|
||||
child: GridFilterPropertyCell(
|
||||
fieldInfo: fieldInfo,
|
||||
onTap: (fieldInfo) => createFilter(fieldInfo),
|
||||
),
|
||||
@@ -147,10 +147,10 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterPropertyCell extends StatelessWidget {
|
||||
class GridFilterPropertyCell extends StatelessWidget {
|
||||
final FieldInfo fieldInfo;
|
||||
final Function(FieldInfo) onTap;
|
||||
const _FilterPropertyCell({
|
||||
const GridFilterPropertyCell({
|
||||
required this.fieldInfo,
|
||||
required this.onTap,
|
||||
Key? key,
|
||||
|
||||
+38
-25
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -12,37 +13,49 @@ import 'create_filter_list.dart';
|
||||
import 'filter_menu_item.dart';
|
||||
|
||||
class FilterMenu extends StatelessWidget {
|
||||
const FilterMenu({Key? key}) : super(key: key);
|
||||
final FieldController fieldController;
|
||||
const FilterMenu({
|
||||
required this.fieldController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [];
|
||||
children.addAll(
|
||||
state.filters
|
||||
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
|
||||
.toList(),
|
||||
);
|
||||
return BlocProvider<GridFilterMenuBloc>(
|
||||
create: (context) => GridFilterMenuBloc(
|
||||
viewId: fieldController.viewId,
|
||||
fieldController: fieldController,
|
||||
)..add(
|
||||
const GridFilterMenuEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [];
|
||||
children.addAll(
|
||||
state.filters
|
||||
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
if (state.creatableFields.isNotEmpty) {
|
||||
children.add(AddFilterButton(viewId: state.viewId));
|
||||
}
|
||||
if (state.creatableFields.isNotEmpty) {
|
||||
children.add(AddFilterButton(viewId: state.viewId));
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: children,
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
@@ -27,3 +28,19 @@ class GridAddRowButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GridRowBottomBar extends StatelessWidget {
|
||||
const GridRowBottomBar({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: GridSize.footerContentInsets,
|
||||
height: GridSize.footerHeight,
|
||||
margin: const EdgeInsets.only(bottom: 200),
|
||||
child: const GridAddRowButton(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -169,17 +169,14 @@ class FieldActionCell extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
disable: !enable,
|
||||
text: FlowyText.medium(
|
||||
action.title(),
|
||||
color: enable
|
||||
? AFThemeExtension.of(context).textColor
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
onTap: () {
|
||||
if (enable) {
|
||||
action.run(context, fieldInfo);
|
||||
}
|
||||
},
|
||||
onTap: () => action.run(context, fieldInfo),
|
||||
leftIcon: svgWidget(
|
||||
action.iconName(),
|
||||
color: enable
|
||||
|
||||
+8
-8
@@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:dartz/dartz.dart' show none;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
@@ -232,15 +233,16 @@ class _DeleteFieldButton extends StatelessWidget {
|
||||
LocaleKeys.grid_field_delete.tr(),
|
||||
color: enable ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
leftIcon: svgWidget(
|
||||
'grid/delete',
|
||||
color: enable ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
onTap: () {
|
||||
if (enable) onDeleted?.call();
|
||||
},
|
||||
onHover: (_) => popoverMutex.close(),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: SizedBox(height: GridSize.popoverItemHeight, child: button),
|
||||
);
|
||||
return SizedBox(height: GridSize.popoverItemHeight, child: button);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -265,13 +267,11 @@ class _HideFieldButton extends StatelessWidget {
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_hide.tr(),
|
||||
),
|
||||
leftIcon: svgWidget('grid/hide'),
|
||||
onTap: () => onHidden?.call(),
|
||||
onHover: (_) => popoverMutex.close(),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: SizedBox(height: GridSize.popoverItemHeight, child: button),
|
||||
);
|
||||
return SizedBox(height: GridSize.popoverItemHeight, child: button);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
-1
@@ -115,7 +115,6 @@ class SwitchFieldButton extends StatelessWidget {
|
||||
text: FlowyText.medium(
|
||||
bloc.state.field.fieldType.title(),
|
||||
),
|
||||
margin: GridSize.typeOptionContentInsets,
|
||||
leftIcon: FlowySvg(name: bloc.state.field.fieldType.iconName()),
|
||||
rightIcon: const FlowySvg(name: 'grid/more'),
|
||||
);
|
||||
|
||||
+4
-15
@@ -93,11 +93,9 @@ class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: DateFormatButton(
|
||||
buttonMargins: GridSize.typeOptionContentInsets,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: DateFormatButton(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -125,10 +123,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: TimeFormatButton(
|
||||
timeFormat: timeFormat,
|
||||
buttonMargins: GridSize.typeOptionContentInsets,
|
||||
),
|
||||
child: TimeFormatButton(timeFormat: timeFormat),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -137,11 +132,9 @@ class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
class DateFormatButton extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final void Function(bool)? onHover;
|
||||
final EdgeInsets? buttonMargins;
|
||||
const DateFormatButton({
|
||||
this.onTap,
|
||||
this.onHover,
|
||||
this.buttonMargins,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -151,7 +144,6 @@ class DateFormatButton extends StatelessWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
|
||||
margin: buttonMargins,
|
||||
onTap: onTap,
|
||||
onHover: onHover,
|
||||
rightIcon: const FlowySvg(name: 'grid/more'),
|
||||
@@ -164,12 +156,10 @@ class TimeFormatButton extends StatelessWidget {
|
||||
final TimeFormatPB timeFormat;
|
||||
final VoidCallback? onTap;
|
||||
final void Function(bool)? onHover;
|
||||
final EdgeInsets? buttonMargins;
|
||||
const TimeFormatButton({
|
||||
required this.timeFormat,
|
||||
this.onTap,
|
||||
this.onHover,
|
||||
this.buttonMargins,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -179,7 +169,6 @@ class TimeFormatButton extends StatelessWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
|
||||
margin: buttonMargins,
|
||||
onTap: onTap,
|
||||
onHover: onHover,
|
||||
rightIcon: const FlowySvg(name: 'grid/more'),
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ class _OptionCellState extends State<_OptionCell> {
|
||||
offset: const Offset(8, 0),
|
||||
margin: EdgeInsets.zero,
|
||||
asBarrier: true,
|
||||
constraints: BoxConstraints.loose(const Size(460, 460)),
|
||||
constraints: BoxConstraints.loose(const Size(460, 470)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: child,
|
||||
|
||||
+6
-6
@@ -59,7 +59,7 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
|
||||
final cells = state.creatableFields.map((fieldInfo) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: _SortPropertyCell(
|
||||
child: GridSortPropertyCell(
|
||||
fieldInfo: fieldInfo,
|
||||
onTap: (fieldInfo) => createSort(fieldInfo),
|
||||
),
|
||||
@@ -69,7 +69,7 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
|
||||
final List<Widget> slivers = [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _FilterTextFieldDelegate(),
|
||||
delegate: _SortTextFieldDelegate(),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: ListView.separated(
|
||||
@@ -109,8 +109,8 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
_FilterTextFieldDelegate();
|
||||
class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
_SortTextFieldDelegate();
|
||||
|
||||
double fixHeight = 46;
|
||||
|
||||
@@ -146,10 +146,10 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
class _SortPropertyCell extends StatelessWidget {
|
||||
class GridSortPropertyCell extends StatelessWidget {
|
||||
final FieldInfo fieldInfo;
|
||||
final Function(FieldInfo) onTap;
|
||||
const _SortPropertyCell({
|
||||
const GridSortPropertyCell({
|
||||
required this.fieldInfo,
|
||||
required this.onTap,
|
||||
Key? key,
|
||||
|
||||
+23
-16
@@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -13,12 +12,9 @@ class OrderPanel extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = SortConditionPB.values.map((condition) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(textFromCondition(condition)),
|
||||
onTap: () => onCondition(condition),
|
||||
),
|
||||
return OrderPannelItem(
|
||||
condition: condition,
|
||||
onCondition: onCondition,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@@ -33,14 +29,25 @@ class OrderPanel extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String textFromCondition(SortConditionPB condition) {
|
||||
switch (condition) {
|
||||
case SortConditionPB.Ascending:
|
||||
return LocaleKeys.grid_sort_ascending.tr();
|
||||
case SortConditionPB.Descending:
|
||||
return LocaleKeys.grid_sort_descending.tr();
|
||||
}
|
||||
return "";
|
||||
class OrderPannelItem extends StatelessWidget {
|
||||
final SortConditionPB condition;
|
||||
final Function(SortConditionPB) onCondition;
|
||||
const OrderPannelItem({
|
||||
required this.condition,
|
||||
required this.onCondition,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(condition.title),
|
||||
onTap: () => onCondition(condition),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
-29
@@ -51,12 +51,12 @@ class _SortEditorState extends State<SortEditor> {
|
||||
child: Column(
|
||||
children: [
|
||||
_SortList(popoverMutex: popoverMutex),
|
||||
_AddSortButton(
|
||||
DatabaseAddSortButton(
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
_DeleteSortButton(popoverMutex: popoverMutex),
|
||||
DatabaseDeleteSortButton(popoverMutex: popoverMutex),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -79,7 +79,7 @@ class _SortList extends StatelessWidget {
|
||||
.map(
|
||||
(info) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: _SortItem(
|
||||
child: DatabaseSortItem(
|
||||
sortInfo: info,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
@@ -95,10 +95,10 @@ class _SortList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SortItem extends StatelessWidget {
|
||||
class DatabaseSortItem extends StatelessWidget {
|
||||
final SortInfo sortInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
const _SortItem({
|
||||
const DatabaseSortItem({
|
||||
required this.popoverMutex,
|
||||
required this.sortInfo,
|
||||
Key? key,
|
||||
@@ -111,7 +111,7 @@ class _SortItem extends StatelessWidget {
|
||||
editable: false,
|
||||
onTap: () {},
|
||||
);
|
||||
final orderButton = _OrderButton(
|
||||
final orderButton = DatabaseSortItemOrderButton(
|
||||
sortInfo: sortInfo,
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
@@ -141,9 +141,11 @@ class _SortItem extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String textFromCondition(SortConditionPB condition) {
|
||||
switch (condition) {
|
||||
extension SortConditionExtension on SortConditionPB {
|
||||
String get title {
|
||||
switch (this) {
|
||||
case SortConditionPB.Ascending:
|
||||
return LocaleKeys.grid_sort_ascending.tr();
|
||||
case SortConditionPB.Descending:
|
||||
@@ -153,11 +155,11 @@ class _SortItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AddSortButton extends StatefulWidget {
|
||||
class DatabaseAddSortButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
final FieldController fieldController;
|
||||
final PopoverMutex popoverMutex;
|
||||
const _AddSortButton({
|
||||
const DatabaseAddSortButton({
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
required this.popoverMutex,
|
||||
@@ -165,10 +167,10 @@ class _AddSortButton extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_AddSortButton> createState() => _AddSortButtonState();
|
||||
State<DatabaseAddSortButton> createState() => _DatabaseAddSortButtonState();
|
||||
}
|
||||
|
||||
class _AddSortButtonState extends State<_AddSortButton> {
|
||||
class _DatabaseAddSortButtonState extends State<DatabaseAddSortButton> {
|
||||
final _popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
@@ -202,9 +204,9 @@ class _AddSortButtonState extends State<_AddSortButton> {
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteSortButton extends StatelessWidget {
|
||||
class DatabaseDeleteSortButton extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
const _DeleteSortButton({required this.popoverMutex, Key? key})
|
||||
const DatabaseDeleteSortButton({required this.popoverMutex, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -228,20 +230,22 @@ class _DeleteSortButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _OrderButton extends StatefulWidget {
|
||||
class DatabaseSortItemOrderButton extends StatefulWidget {
|
||||
final SortInfo sortInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
const _OrderButton({
|
||||
const DatabaseSortItemOrderButton({
|
||||
required this.popoverMutex,
|
||||
required this.sortInfo,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_OrderButtonState createState() => _OrderButtonState();
|
||||
State<DatabaseSortItemOrderButton> createState() =>
|
||||
_DatabaseSortItemOrderButtonState();
|
||||
}
|
||||
|
||||
class _OrderButtonState extends State<_OrderButton> {
|
||||
class _DatabaseSortItemOrderButtonState
|
||||
extends State<DatabaseSortItemOrderButton> {
|
||||
final PopoverController popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
@@ -268,20 +272,10 @@ class _OrderButtonState extends State<_OrderButton> {
|
||||
);
|
||||
},
|
||||
child: SortChoiceButton(
|
||||
text: textFromCondition(widget.sortInfo.sortPB.condition),
|
||||
text: widget.sortInfo.sortPB.condition.title,
|
||||
rightIcon: arrow,
|
||||
onTap: () => popoverController.show(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String textFromCondition(SortConditionPB condition) {
|
||||
switch (condition) {
|
||||
case SortConditionPB.Ascending:
|
||||
return LocaleKeys.grid_sort_ascending.tr();
|
||||
case SortConditionPB.Descending:
|
||||
return LocaleKeys.grid_sort_descending.tr();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
+32
-21
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -13,30 +14,40 @@ import 'sort_editor.dart';
|
||||
import 'sort_info.dart';
|
||||
|
||||
class SortMenu extends StatelessWidget {
|
||||
const SortMenu({Key? key}) : super(key: key);
|
||||
final FieldController fieldController;
|
||||
const SortMenu({
|
||||
required this.fieldController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SortMenuBloc, SortMenuState>(
|
||||
builder: (context, state) {
|
||||
if (state.sortInfos.isNotEmpty) {
|
||||
return AppFlowyPopover(
|
||||
controller: PopoverController(),
|
||||
constraints: BoxConstraints.loose(const Size(340, 200)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return SortEditor(
|
||||
viewId: state.viewId,
|
||||
fieldController: context.read<SortMenuBloc>().fieldController,
|
||||
sortInfos: state.sortInfos,
|
||||
);
|
||||
},
|
||||
child: SortChoiceChip(sortInfos: state.sortInfos),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
return BlocProvider<SortMenuBloc>(
|
||||
create: (context) => SortMenuBloc(
|
||||
viewId: fieldController.viewId,
|
||||
fieldController: fieldController,
|
||||
)..add(const SortMenuEvent.initial()),
|
||||
child: BlocBuilder<SortMenuBloc, SortMenuState>(
|
||||
builder: (context, state) {
|
||||
if (state.sortInfos.isNotEmpty) {
|
||||
return AppFlowyPopover(
|
||||
controller: PopoverController(),
|
||||
constraints: BoxConstraints.loose(const Size(340, 200)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return SortEditor(
|
||||
viewId: state.viewId,
|
||||
fieldController: context.read<SortMenuBloc>().fieldController,
|
||||
sortInfos: state.sortInfos,
|
||||
);
|
||||
},
|
||||
child: SortChoiceChip(sortInfos: state.sortInfos),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-30
@@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -67,34 +66,6 @@ class _DatabaseLayoutListState extends State<DatabaseLayoutList> {
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseLayoutExtension on DatabaseLayoutPB {
|
||||
String layoutName() {
|
||||
switch (this) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return LocaleKeys.board_menuName.tr();
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
return LocaleKeys.calendar_menuName.tr();
|
||||
case DatabaseLayoutPB.Grid:
|
||||
return LocaleKeys.grid_menuName.tr();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
String iconName() {
|
||||
switch (this) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return 'editor/board';
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
return "editor/grid";
|
||||
case DatabaseLayoutPB.Grid:
|
||||
return "editor/grid";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseViewLayoutCell extends StatelessWidget {
|
||||
final bool isSelected;
|
||||
final DatabaseLayoutPB databaseLayout;
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'filter_button.dart';
|
||||
import 'sort_button.dart';
|
||||
|
||||
class GridSettingBar extends StatelessWidget {
|
||||
final DatabaseController controller;
|
||||
final ToggleExtensionNotifier toggleExtension;
|
||||
const GridSettingBar({
|
||||
required this.controller,
|
||||
required this.toggleExtension,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<GridFilterMenuBloc>(
|
||||
create: (context) => GridFilterMenuBloc(
|
||||
viewId: controller.viewId,
|
||||
fieldController: controller.fieldController,
|
||||
)..add(const GridFilterMenuEvent.initial()),
|
||||
),
|
||||
BlocProvider<SortMenuBloc>(
|
||||
create: (context) => SortMenuBloc(
|
||||
viewId: controller.viewId,
|
||||
fieldController: controller.fieldController,
|
||||
)..add(const SortMenuEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
|
||||
listenWhen: (p, c) => p.isVisible != c.isVisible,
|
||||
listener: (context, state) => toggleExtension.toggle(),
|
||||
),
|
||||
BlocListener<SortMenuBloc, SortMenuState>(
|
||||
listenWhen: (p, c) => p.isVisible != c.isVisible,
|
||||
listener: (context, state) => toggleExtension.toggle(),
|
||||
),
|
||||
],
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(width: GridSize.leadingHeaderPadding),
|
||||
const Spacer(),
|
||||
const FilterButton(),
|
||||
const SortButton(),
|
||||
SettingButton(
|
||||
databaseController: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../layout/sizes.dart';
|
||||
import 'filter_button.dart';
|
||||
import '../../../../widgets/setting/setting_button.dart';
|
||||
import 'sort_button.dart';
|
||||
|
||||
class GridToolbarContext {
|
||||
final String viewId;
|
||||
final FieldController fieldController;
|
||||
GridToolbarContext({
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
});
|
||||
}
|
||||
|
||||
class GridToolbar extends StatelessWidget {
|
||||
const GridToolbar({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(width: GridSize.leadingHeaderPadding),
|
||||
const Spacer(),
|
||||
const FilterButton(),
|
||||
const SortButton(),
|
||||
SettingButton(
|
||||
databaseController: context.read<GridBloc>().databaseController,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../application/field/field_controller.dart';
|
||||
import '../grid/presentation/layout/sizes.dart';
|
||||
import '../grid/presentation/widgets/filter/filter_menu.dart';
|
||||
import '../grid/presentation/widgets/sort/sort_menu.dart';
|
||||
|
||||
class DatabaseViewSettingExtension extends StatelessWidget {
|
||||
final String viewId;
|
||||
final DatabaseController databaseController;
|
||||
final ToggleExtensionNotifier toggleExtension;
|
||||
const DatabaseViewSettingExtension({
|
||||
required this.viewId,
|
||||
required this.databaseController,
|
||||
required this.toggleExtension,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: toggleExtension,
|
||||
child: Consumer<ToggleExtensionNotifier>(
|
||||
builder: (context, value, child) {
|
||||
if (value.isToggled) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
DatabaseViewSettingExtensionBloc(viewId: viewId),
|
||||
child: _DatabaseViewSettingContent(
|
||||
fieldController: databaseController.fieldController,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DatabaseViewSettingContent extends StatelessWidget {
|
||||
final FieldController fieldController;
|
||||
const _DatabaseViewSettingContent({
|
||||
required this.fieldController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DatabaseViewSettingExtensionBloc,
|
||||
DatabaseViewSettingExtensionState>(
|
||||
builder: (context, state) {
|
||||
final children = <Widget>[
|
||||
Divider(
|
||||
height: 1.0,
|
||||
color: AFThemeExtension.of(context).toggleOffFill,
|
||||
),
|
||||
const VSpace(6),
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
SortMenu(
|
||||
fieldController: fieldController,
|
||||
),
|
||||
const HSpace(6),
|
||||
FilterMenu(
|
||||
fieldController: fieldController,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
return _wrapPadding(
|
||||
Column(children: children),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapPadding(Widget child) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.leadingHeaderPadding,
|
||||
vertical: 6,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../application/database_controller.dart';
|
||||
import '../grid/presentation/layout/sizes.dart';
|
||||
import 'tar_bar_add_button.dart';
|
||||
|
||||
abstract class DatabaseTabBarItemBuilder {
|
||||
const DatabaseTabBarItemBuilder();
|
||||
|
||||
/// Returns the content of the tab bar item. The content is shown when the tab
|
||||
/// bar item is selected. It can be any kind of database view.
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
);
|
||||
|
||||
/// Returns the setting bar of the tab bar item. The setting bar is shown on the
|
||||
/// top right conner when the tab bar item is selected.
|
||||
Widget settingBar(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
);
|
||||
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
);
|
||||
}
|
||||
|
||||
class DatabaseTabBarView extends StatefulWidget {
|
||||
final ViewPB view;
|
||||
const DatabaseTabBarView({
|
||||
required this.view,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DatabaseTabBarView> createState() => _DatabaseTabBarViewState();
|
||||
}
|
||||
|
||||
class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
|
||||
PageController? _pageController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(
|
||||
initialPage: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<GridTabBarBloc>(
|
||||
create: (context) => GridTabBarBloc(view: widget.view)
|
||||
..add(
|
||||
const GridTabBarEvent.initial(),
|
||||
),
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<GridTabBarBloc, GridTabBarState>(
|
||||
listenWhen: (p, c) => p.selectedIndex != c.selectedIndex,
|
||||
listener: (context, state) {
|
||||
_pageController?.animateToPage(
|
||||
state.selectedIndex,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||
builder: (context, state) {
|
||||
return const Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 50),
|
||||
child: DatabaseTabBar(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
width: 300,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 50),
|
||||
child: pageSettingBarFromState(state),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||
builder: (context, state) {
|
||||
return pageSettingBarExtensionFromState(state);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||
builder: (context, state) {
|
||||
return PageView(
|
||||
pageSnapping: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _pageController,
|
||||
children: pageContentFromState(state),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> pageContentFromState(GridTabBarState state) {
|
||||
return state.tabBars.map((tabBar) {
|
||||
final controller =
|
||||
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
|
||||
return tabBar.builder.content(
|
||||
context,
|
||||
tabBar.view,
|
||||
controller,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget pageSettingBarFromState(GridTabBarState state) {
|
||||
if (state.tabBars.length < state.selectedIndex) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final tarBar = state.tabBars[state.selectedIndex];
|
||||
final controller =
|
||||
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
|
||||
return tarBar.builder.settingBar(
|
||||
context,
|
||||
controller,
|
||||
);
|
||||
}
|
||||
|
||||
Widget pageSettingBarExtensionFromState(GridTabBarState state) {
|
||||
if (state.tabBars.length < state.selectedIndex) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final tarBar = state.tabBars[state.selectedIndex];
|
||||
final controller =
|
||||
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
|
||||
return tarBar.builder.settingBarExtension(
|
||||
context,
|
||||
controller,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseTabBarViewPlugin extends Plugin {
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
final PluginType _pluginType;
|
||||
|
||||
DatabaseTabBarViewPlugin({
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(view: view);
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder(
|
||||
notifier: notifier,
|
||||
);
|
||||
|
||||
@override
|
||||
PluginId get id => notifier.view.id;
|
||||
|
||||
@override
|
||||
PluginType get pluginType => _pluginType;
|
||||
}
|
||||
|
||||
class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
final ViewPluginNotifier notifier;
|
||||
|
||||
DatabasePluginWidgetBuilder({
|
||||
required this.notifier,
|
||||
Key? key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
|
||||
|
||||
@override
|
||||
Widget buildWidget({PluginContext? context}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||
if (deletedView.hasIndex()) {
|
||||
context?.onDeleted(notifier.view, deletedView.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
return DatabaseTabBarView(
|
||||
key: ValueKey(notifier.view.id),
|
||||
view: notifier.view,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
}
|
||||
|
||||
class DatabaseTabBar extends StatefulWidget {
|
||||
const DatabaseTabBar({super.key});
|
||||
|
||||
@override
|
||||
State<DatabaseTabBar> createState() => _DatabaseTabBarState();
|
||||
}
|
||||
|
||||
class _DatabaseTabBarState extends State<DatabaseTabBar> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||
builder: (context, state) {
|
||||
final children = state.tabBars.indexed.map((indexed) {
|
||||
final isSelected = state.selectedIndex == indexed.$1;
|
||||
final tabBar = indexed.$2;
|
||||
return DatabaseTabBarItem(
|
||||
key: ValueKey(tabBar.viewId),
|
||||
view: tabBar.view,
|
||||
isSelected: isSelected,
|
||||
onTap: (selectedView) {
|
||||
context.read<GridTabBarBloc>().add(
|
||||
GridTabBarEvent.selectView(selectedView.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: IntrinsicWidth(
|
||||
child: Row(children: children),
|
||||
),
|
||||
),
|
||||
),
|
||||
AddDatabaseViewButton(
|
||||
onTap: (action) async {
|
||||
context.read<GridTabBarBloc>().add(
|
||||
GridTabBarEvent.createView(action),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseTabBarItem extends StatelessWidget {
|
||||
final bool isSelected;
|
||||
final ViewPB view;
|
||||
final Function(ViewPB) onTap;
|
||||
const DatabaseTabBarItem({
|
||||
required this.view,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 80, maxWidth: 160),
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
children: [
|
||||
TabBarItemButton(
|
||||
view: view,
|
||||
onTap: () => onTap(view),
|
||||
),
|
||||
if (isSelected)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 2,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarItemButton extends StatelessWidget {
|
||||
final ViewPB view;
|
||||
final VoidCallback onTap;
|
||||
const TabBarItemButton({
|
||||
required this.view,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopoverActionList<TabBarViewAction>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: TabBarViewAction.values,
|
||||
buildChild: (controller) {
|
||||
return FlowyButton(
|
||||
radius: Corners.s5Border,
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
onTap: onTap,
|
||||
onSecondaryTap: () {
|
||||
controller.show();
|
||||
},
|
||||
text: FlowyText.medium(
|
||||
view.name,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
margin: GridSize.cellContentInsets,
|
||||
leftIcon: svgWidget(
|
||||
view.iconName,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
switch (action) {
|
||||
case TabBarViewAction.rename:
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
|
||||
value: view.name,
|
||||
confirm: (newValue) {
|
||||
context.read<GridTabBarBloc>().add(
|
||||
GridTabBarEvent.renameView(view.id, newValue),
|
||||
);
|
||||
},
|
||||
).show(context);
|
||||
break;
|
||||
case TabBarViewAction.delete:
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_deleteView.tr(),
|
||||
confirm: () {
|
||||
context.read<GridTabBarBloc>().add(
|
||||
GridTabBarEvent.deleteView(view.id),
|
||||
);
|
||||
},
|
||||
).show(context);
|
||||
|
||||
break;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum TabBarViewAction implements ActionCell {
|
||||
rename,
|
||||
delete;
|
||||
|
||||
@override
|
||||
String get name {
|
||||
switch (this) {
|
||||
case TabBarViewAction.rename:
|
||||
return LocaleKeys.disclosureAction_rename.tr();
|
||||
case TabBarViewAction.delete:
|
||||
return LocaleKeys.disclosureAction_delete.tr();
|
||||
}
|
||||
}
|
||||
|
||||
Widget icon(Color iconColor) {
|
||||
switch (this) {
|
||||
case TabBarViewAction.rename:
|
||||
return const FlowySvg(name: 'editor/edit');
|
||||
case TabBarViewAction.delete:
|
||||
return const FlowySvg(name: 'editor/delete');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) => icon(iconColor);
|
||||
|
||||
@override
|
||||
Widget? rightIcon(Color iconColor) => null;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AddDatabaseViewButton extends StatefulWidget {
|
||||
final Function(AddButtonAction) onTap;
|
||||
const AddDatabaseViewButton({
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddDatabaseViewButton> createState() => _AddDatabaseViewButtonState();
|
||||
}
|
||||
|
||||
class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
|
||||
final popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(200, 400)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
offset: const Offset(0, 8),
|
||||
margin: EdgeInsets.zero,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: FlowyIconButton(
|
||||
iconPadding: const EdgeInsets.all(4),
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
onPressed: () => popoverController.show(),
|
||||
icon: svgWidget(
|
||||
'home/add',
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return TarBarAddButtonAction(
|
||||
onTap: (action) {
|
||||
popoverController.close();
|
||||
widget.onTap(action);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TarBarAddButtonAction extends StatelessWidget {
|
||||
final Function(AddButtonAction) onTap;
|
||||
const TarBarAddButtonAction({
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = AddButtonAction.values.map((layout) {
|
||||
return TarBarAddButtonActionCell(
|
||||
action: layout,
|
||||
onTap: onTap,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return ListView.separated(
|
||||
controller: ScrollController(),
|
||||
shrinkWrap: true,
|
||||
itemCount: cells.length,
|
||||
itemBuilder: (BuildContext context, int index) => cells[index],
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TarBarAddButtonActionCell extends StatelessWidget {
|
||||
final AddButtonAction action;
|
||||
final void Function(AddButtonAction) onTap;
|
||||
const TarBarAddButtonActionCell({
|
||||
required this.action,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
'${LocaleKeys.grid_createView.tr()} ${action.title}',
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: svgWidget(
|
||||
action.iconName,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onTap: () => onTap(action),
|
||||
).padding(horizontal: 6.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum AddButtonAction {
|
||||
grid,
|
||||
calendar,
|
||||
board;
|
||||
|
||||
String get title {
|
||||
switch (this) {
|
||||
case AddButtonAction.board:
|
||||
return LocaleKeys.board_menuName.tr();
|
||||
case AddButtonAction.calendar:
|
||||
return LocaleKeys.calendar_menuName.tr();
|
||||
case AddButtonAction.grid:
|
||||
return LocaleKeys.grid_menuName.tr();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
ViewLayoutPB get layoutType {
|
||||
switch (this) {
|
||||
case AddButtonAction.board:
|
||||
return ViewLayoutPB.Board;
|
||||
case AddButtonAction.calendar:
|
||||
return ViewLayoutPB.Calendar;
|
||||
case AddButtonAction.grid:
|
||||
return ViewLayoutPB.Grid;
|
||||
default:
|
||||
return ViewLayoutPB.Grid;
|
||||
}
|
||||
}
|
||||
|
||||
String get iconName {
|
||||
switch (this) {
|
||||
case AddButtonAction.board:
|
||||
return 'editor/board';
|
||||
case AddButtonAction.calendar:
|
||||
return "editor/grid";
|
||||
case AddButtonAction.grid:
|
||||
return "editor/grid";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
extension DatabaseLayoutExtension on DatabaseLayoutPB {
|
||||
String layoutName() {
|
||||
switch (this) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return LocaleKeys.board_menuName.tr();
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
return LocaleKeys.calendar_menuName.tr();
|
||||
case DatabaseLayoutPB.Grid:
|
||||
return LocaleKeys.grid_menuName.tr();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
String iconName() {
|
||||
switch (this) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return 'editor/board';
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
return "editor/grid";
|
||||
case DatabaseLayoutPB.Grid:
|
||||
return "editor/grid";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ class GridCellBuilder {
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
style: style,
|
||||
fieldType: cellContext.fieldType,
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
@@ -50,6 +51,7 @@ class GridCellBuilder {
|
||||
key: key,
|
||||
editable: false,
|
||||
style: style,
|
||||
fieldType: cellContext.fieldType,
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return GridSingleSelectCell(
|
||||
|
||||
+36
-11
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -22,11 +23,17 @@ abstract class GridCellDelegate {
|
||||
|
||||
class GridDateCell extends GridCellWidget {
|
||||
final bool editable;
|
||||
|
||||
/// The [GridDateCell] is used by Field Type [FieldType.DateTime],
|
||||
/// [FieldType.CreatedTime], [FieldType.LastEditedTime]. So it needs
|
||||
/// to know the field type.
|
||||
final FieldType fieldType;
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
late final DateCellStyle? cellStyle;
|
||||
|
||||
GridDateCell({
|
||||
GridCellStyle? style,
|
||||
required this.fieldType,
|
||||
required this.cellControllerBuilder,
|
||||
this.editable = true,
|
||||
Key? key,
|
||||
@@ -65,17 +72,9 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||
builder: (context, state) {
|
||||
Widget dateTextWidget = SizedBox.expand(
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: FlowyText.medium(
|
||||
state.dateStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Widget dateTextWidget = GridDateCellText(
|
||||
dateStr: state.dateStr,
|
||||
alignment: alignment,
|
||||
);
|
||||
|
||||
// If the cell is editable, wrap it in a popover.
|
||||
@@ -123,3 +122,29 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.dateStr;
|
||||
}
|
||||
|
||||
class GridDateCellText extends StatelessWidget {
|
||||
final String dateStr;
|
||||
final Alignment alignment;
|
||||
const GridDateCellText({
|
||||
required this.dateStr,
|
||||
required this.alignment,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.expand(
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: FlowyText.medium(
|
||||
dateStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -337,7 +337,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
offset: const Offset(8, 0),
|
||||
margin: EdgeInsets.zero,
|
||||
asBarrier: true,
|
||||
constraints: BoxConstraints.loose(const Size(200, 460)),
|
||||
constraints: BoxConstraints.loose(const Size(200, 470)),
|
||||
mutex: widget.popoverMutex,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
|
||||
@@ -186,7 +186,7 @@ class EmojiButton extends StatelessWidget {
|
||||
height: _kBannerActionHeight,
|
||||
width: _kBannerActionHeight,
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.all(4),
|
||||
margin: EdgeInsets.zero,
|
||||
text: FlowyText.medium(
|
||||
emoji,
|
||||
fontSize: 30,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -35,8 +37,9 @@ class RowDocument extends StatelessWidget {
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
error: (error) => FlowyErrorPage(
|
||||
error: (error) => FlowyErrorPage.message(
|
||||
error.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
finish: () => RowEditor(
|
||||
viewPB: state.viewPB!,
|
||||
@@ -94,8 +97,9 @@ class _RowEditorState extends State<RowEditor> {
|
||||
),
|
||||
finish: (result) {
|
||||
return result.fold(
|
||||
(error) => FlowyErrorPage(
|
||||
(error) => FlowyErrorPage.message(
|
||||
error.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
(_) {
|
||||
final editorState = documentBloc.editorState;
|
||||
|
||||
+1
-1
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
@@ -9,6 +8,7 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import 'setting_button.dart';
|
||||
|
||||
class DatabaseSettingList extends StatelessWidget {
|
||||
final DatabaseController databaseContoller;
|
||||
|
||||
+63
-3
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@@ -97,7 +97,7 @@ class _DatabaseSettingListPopoverState
|
||||
case DatabaseSettingAction.showLayout:
|
||||
return DatabaseLayoutList(
|
||||
viewId: widget.databaseController.viewId,
|
||||
currentLayout: widget.databaseController.databaseLayout!,
|
||||
currentLayout: widget.databaseController.databaseLayout,
|
||||
);
|
||||
case DatabaseSettingAction.showGroup:
|
||||
return DatabaseGroupList(
|
||||
@@ -132,7 +132,7 @@ class ICalendarSettingImpl extends ICalendarSetting {
|
||||
|
||||
@override
|
||||
void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) {
|
||||
_databaseController.updateCalenderLayoutSetting(layoutSettings);
|
||||
_databaseController.updateLayoutSetting(layoutSettings);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -140,3 +140,63 @@ class ICalendarSettingImpl extends ICalendarSetting {
|
||||
return _databaseController.databaseLayoutSetting?.calendar;
|
||||
}
|
||||
}
|
||||
|
||||
enum DatabaseSettingAction {
|
||||
showProperties,
|
||||
showLayout,
|
||||
showGroup,
|
||||
showCalendarLayout,
|
||||
}
|
||||
|
||||
extension DatabaseSettingActionExtension on DatabaseSettingAction {
|
||||
String iconName() {
|
||||
switch (this) {
|
||||
case DatabaseSettingAction.showProperties:
|
||||
return 'grid/setting/properties';
|
||||
case DatabaseSettingAction.showLayout:
|
||||
return 'grid/setting/database_layout';
|
||||
case DatabaseSettingAction.showGroup:
|
||||
return 'grid/setting/group';
|
||||
case DatabaseSettingAction.showCalendarLayout:
|
||||
return 'grid/setting/calendar_layout';
|
||||
}
|
||||
}
|
||||
|
||||
String title() {
|
||||
switch (this) {
|
||||
case DatabaseSettingAction.showProperties:
|
||||
return LocaleKeys.grid_settings_Properties.tr();
|
||||
case DatabaseSettingAction.showLayout:
|
||||
return LocaleKeys.grid_settings_databaseLayout.tr();
|
||||
case DatabaseSettingAction.showGroup:
|
||||
return LocaleKeys.grid_settings_group.tr();
|
||||
case DatabaseSettingAction.showCalendarLayout:
|
||||
return LocaleKeys.calendar_settings_name.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of actions that should be shown for the given database layout.
|
||||
List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
|
||||
switch (layout) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return [
|
||||
DatabaseSettingAction.showProperties,
|
||||
DatabaseSettingAction.showLayout,
|
||||
DatabaseSettingAction.showGroup,
|
||||
];
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
return [
|
||||
DatabaseSettingAction.showProperties,
|
||||
DatabaseSettingAction.showLayout,
|
||||
DatabaseSettingAction.showCalendarLayout,
|
||||
];
|
||||
case DatabaseLayoutPB.Grid:
|
||||
return [
|
||||
DatabaseSettingAction.showProperties,
|
||||
DatabaseSettingAction.showLayout,
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show EditorState, LogLevel;
|
||||
show EditorState, LogLevel, TransactionTime;
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -149,8 +149,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
this.editorState = editorState;
|
||||
|
||||
// subscribe to the document change from the editor
|
||||
_subscription = editorState.transactionStream.listen((transaction) async {
|
||||
await _transactionAdapter.apply(transaction, editorState);
|
||||
_subscription = editorState.transactionStream.listen((event) async {
|
||||
final time = event.$1;
|
||||
if (time != TransactionTime.before) {
|
||||
return;
|
||||
}
|
||||
await _transactionAdapter.apply(event.$2, editorState);
|
||||
});
|
||||
|
||||
// output the log from the editor when debug mode
|
||||
|
||||
+2
-1
@@ -117,12 +117,13 @@ extension NodeToBlock on Node {
|
||||
BlockPB toBlock({
|
||||
String? parentId,
|
||||
String? childrenId,
|
||||
Attributes? attributes,
|
||||
}) {
|
||||
assert(id.isNotEmpty);
|
||||
final block = BlockPB.create()
|
||||
..id = id
|
||||
..ty = type
|
||||
..data = _dataAdapter(type, attributes);
|
||||
..data = _dataAdapter(type, attributes ?? this.attributes);
|
||||
if (childrenId != null && childrenId.isNotEmpty) {
|
||||
block.childrenId = childrenId;
|
||||
}
|
||||
|
||||
+11
-4
@@ -10,7 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
UpdateOperation,
|
||||
DeleteOperation,
|
||||
PathExtensions,
|
||||
Node;
|
||||
Node,
|
||||
composeAttributes;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:async';
|
||||
|
||||
@@ -34,7 +35,8 @@ class TransactionAdapter {
|
||||
final actions = transaction.operations
|
||||
.map((op) => op.toBlockAction(editorState))
|
||||
.whereNotNull()
|
||||
.expand((element) => element);
|
||||
.expand((element) => element)
|
||||
.toList(growable: false); // avoid lazy evaluation
|
||||
// Log.debug('actions => $actions');
|
||||
await documentService.applyAction(
|
||||
documentId: documentId,
|
||||
@@ -114,7 +116,10 @@ extension on UpdateOperation {
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
assert(parentId.isNotEmpty);
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock()
|
||||
..block = node.toBlock(
|
||||
parentId: parentId,
|
||||
attributes: composeAttributes(oldAttributes, attributes),
|
||||
)
|
||||
..parentId = parentId;
|
||||
actions.add(
|
||||
BlockActionPB()
|
||||
@@ -132,7 +137,9 @@ extension on DeleteOperation {
|
||||
final parentId =
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock()
|
||||
..block = node.toBlock(
|
||||
parentId: parentId,
|
||||
)
|
||||
..parentId = parentId;
|
||||
assert(parentId.isNotEmpty);
|
||||
actions.add(
|
||||
|
||||
@@ -50,10 +50,7 @@ class DocumentPlugin extends Plugin<int> {
|
||||
required ViewPB view,
|
||||
bool listenOnViewChanged = false,
|
||||
Key? key,
|
||||
}) : notifier = ViewPluginNotifier(
|
||||
view: view,
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
) {
|
||||
}) : notifier = ViewPluginNotifier(view: view) {
|
||||
_pluginType = pluginType;
|
||||
_documentAppearanceCubit.fetch();
|
||||
}
|
||||
@@ -126,7 +123,10 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget? get rightBarItem {
|
||||
return Row(
|
||||
children: [
|
||||
DocumentShareButton(view: view),
|
||||
DocumentShareButton(
|
||||
key: ValueKey(view.id),
|
||||
view: view,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
BlocProvider.value(
|
||||
value: documentAppearanceCubit,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
@@ -14,11 +15,11 @@ import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
||||
hide DocumentEvent;
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class DocumentPage extends StatefulWidget {
|
||||
@@ -65,7 +66,10 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
return state.loadingState.when(
|
||||
loading: () => const SizedBox.shrink(),
|
||||
finish: (result) => result.fold(
|
||||
(error) => FlowyErrorPage(error.toString()),
|
||||
(error) => FlowyErrorPage.message(
|
||||
error.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
(data) {
|
||||
if (state.forceClose) {
|
||||
widget.onDeleted();
|
||||
|
||||
@@ -107,6 +107,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
if (widget.scrollController == null) {
|
||||
effectiveScrollController.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+48
-56
@@ -91,10 +91,12 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
onExit: (_) => widget.editorState.service.scrollService?.enable(),
|
||||
child: SizedBox(
|
||||
height: 400,
|
||||
child: Stack(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMenu(context, viewPB),
|
||||
_buildPage(context, viewPB),
|
||||
Expanded(child: _buildPage(context, viewPB)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -114,68 +116,58 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
}
|
||||
|
||||
Widget _buildMenu(BuildContext context, ViewPB viewPB) {
|
||||
return Positioned(
|
||||
top: 5,
|
||||
left: 5,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// information
|
||||
FlowyIconButton(
|
||||
tooltipText: LocaleKeys.tooltip_referencePage.tr(
|
||||
namedArgs: {'name': viewPB.layout.name},
|
||||
),
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// information
|
||||
FlowyIconButton(
|
||||
tooltipText: LocaleKeys.tooltip_referencePage.tr(
|
||||
namedArgs: {'name': viewPB.layout.name},
|
||||
),
|
||||
width: 24,
|
||||
height: 24,
|
||||
iconPadding: const EdgeInsets.all(3),
|
||||
icon: svgWidget(
|
||||
'common/information',
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
// setting
|
||||
const Space(7, 0),
|
||||
PopoverActionList<_ActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: _ActionType.values
|
||||
.map((action) => _ActionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) => FlowyIconButton(
|
||||
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
|
||||
width: 24,
|
||||
height: 24,
|
||||
iconPadding: const EdgeInsets.all(3),
|
||||
icon: svgWidget(
|
||||
'common/information',
|
||||
'common/settings',
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onPressed: () => controller.show(),
|
||||
),
|
||||
// Name
|
||||
const Space(7, 0),
|
||||
FlowyText.medium(
|
||||
viewPB.name,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
// setting
|
||||
const Space(7, 0),
|
||||
PopoverActionList<_ActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: _ActionType.values
|
||||
.map((action) => _ActionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) => FlowyIconButton(
|
||||
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
|
||||
width: 24,
|
||||
height: 24,
|
||||
iconPadding: const EdgeInsets.all(3),
|
||||
icon: svgWidget(
|
||||
'common/settings',
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onPressed: () => controller.show(),
|
||||
),
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case _ActionType.viewDatabase:
|
||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case _ActionType.viewDatabase:
|
||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
||||
|
||||
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
||||
break;
|
||||
case _ActionType.delete:
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.deleteNode(widget.node);
|
||||
widget.editorState.apply(transaction);
|
||||
break;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
||||
break;
|
||||
case _ActionType.delete:
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.deleteNode(widget.node);
|
||||
widget.editorState.apply(transaction);
|
||||
break;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+17
-7
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -31,14 +32,22 @@ extension InsertDatabase on EditorState {
|
||||
await apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> insertReferencePage(ViewPB childView) async {
|
||||
Future<void> insertReferencePage(
|
||||
ViewPB childView,
|
||||
) async {
|
||||
final selection = this.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
throw FlowyError(
|
||||
msg:
|
||||
"Could not insert the reference page because the current selection was null or collapsed.",
|
||||
);
|
||||
}
|
||||
final node = getNodeAtPath(selection.end.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
throw FlowyError(
|
||||
msg:
|
||||
"Could not insert the reference page because the current node at the selection does not exist.",
|
||||
);
|
||||
}
|
||||
|
||||
// get the database id that the view is associated with
|
||||
@@ -53,17 +62,18 @@ extension InsertDatabase on EditorState {
|
||||
}
|
||||
|
||||
final prefix = _referencedDatabasePrefix(childView.layout);
|
||||
final ref = await ViewBackendService.createDatabaseReferenceView(
|
||||
final ref = await ViewBackendService.createDatabaseLinkedView(
|
||||
parentViewId: childView.id,
|
||||
name: "$prefix ${childView.name}",
|
||||
layoutType: childView.layout,
|
||||
databaseId: databaseId,
|
||||
).then((value) => value.swap().toOption().toNullable());
|
||||
|
||||
// TODO(a-wallen): Show error dialog here.
|
||||
// Maybe extend the FlowyErrorPage.
|
||||
if (ref == null) {
|
||||
return;
|
||||
throw FlowyError(
|
||||
msg:
|
||||
"The `ViewBackendService` failed to create a database reference view",
|
||||
);
|
||||
}
|
||||
|
||||
final transaction = this.transaction;
|
||||
|
||||
+18
-17
@@ -1,10 +1,14 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@@ -36,9 +40,19 @@ void showLinkToPageMenu(
|
||||
editorState: editorState,
|
||||
layoutType: pageType,
|
||||
hintText: pageType.toHintText(),
|
||||
onSelected: (appPB, viewPB) {
|
||||
editorState.insertReferencePage(viewPB);
|
||||
linkToPageMenuEntry.remove();
|
||||
onSelected: (appPB, viewPB) async {
|
||||
try {
|
||||
await editorState.insertReferencePage(viewPB);
|
||||
linkToPageMenuEntry.remove();
|
||||
} on FlowyError catch (e) {
|
||||
Dialogs.show(
|
||||
FlowyErrorPage.message(
|
||||
e.msg,
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
context,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -212,7 +226,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
FlowyButton(
|
||||
isSelected: index == _selectedIndex,
|
||||
leftIcon: svgWidget(
|
||||
_iconName(value),
|
||||
value.iconName,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
text: FlowyText.regular(value.name),
|
||||
@@ -238,19 +252,6 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
future: items,
|
||||
);
|
||||
}
|
||||
|
||||
String _iconName(ViewPB viewPB) {
|
||||
switch (viewPB.layout) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return 'editor/grid';
|
||||
case ViewLayoutPB.Board:
|
||||
return 'editor/board';
|
||||
case ViewLayoutPB.Calendar:
|
||||
return 'editor/calendar';
|
||||
default:
|
||||
throw Exception('Unknown layout type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on ViewLayoutPB {
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ class CoverImagePickerBloc
|
||||
}
|
||||
|
||||
Future<String> _coverPath() async {
|
||||
final directory = await getIt<LocalFileStorage>().getPath();
|
||||
final directory = await getIt<ApplicationDataStorage>().getPath();
|
||||
return Directory(p.join(directory, 'covers'))
|
||||
.create(recursive: true)
|
||||
.then((value) => value.path);
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ CharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent(
|
||||
handler: (editorState) async => await formatMarkdownSymbol(
|
||||
editorState,
|
||||
(node) => node.type != ToggleListBlockKeys.type,
|
||||
(text, _) => text == _greater,
|
||||
(_, text, __) => text == _greater,
|
||||
(_, node, delta) => toggleListBlockNode(
|
||||
delta: delta.compose(Delta()..delete(_greater.length)),
|
||||
),
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../workspace/presentation/home/home_stack.dart';
|
||||
|
||||
class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
||||
final ViewListener? _viewListener;
|
||||
ViewPB view;
|
||||
@@ -18,30 +14,18 @@ class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
||||
|
||||
ViewPluginNotifier({
|
||||
required this.view,
|
||||
required bool listenOnViewChanged,
|
||||
}) : _viewListener = ViewListener(viewId: view.id) {
|
||||
if (listenOnViewChanged) {
|
||||
_viewListener?.start(
|
||||
onViewUpdated: (updatedView) {
|
||||
// If the layout is changed, we need to create a new plugin for it.
|
||||
if (view.layout != updatedView.layout) {
|
||||
getIt<HomeStackManager>().setPlugin(
|
||||
updatedView.plugin(
|
||||
listenOnViewChanged: listenOnViewChanged,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
view = updatedView;
|
||||
}
|
||||
},
|
||||
onViewMoveToTrash: (result) {
|
||||
result.fold(
|
||||
(deletedView) => isDeleted.value = some(deletedView),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
_viewListener?.start(
|
||||
onViewUpdated: (updatedView) {
|
||||
view = updatedView;
|
||||
},
|
||||
onViewMoveToTrash: (result) {
|
||||
result.fold(
|
||||
(deletedView) => isDeleted.value = some(deletedView),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -51,7 +51,7 @@ void _resolveCommonService(GetIt getIt) async {
|
||||
// getIt.registerFactory<KeyValueStorage>(() => RustKeyValue());
|
||||
getIt.registerFactory<KeyValueStorage>(() => DartKeyValue());
|
||||
getIt.registerFactory<FilePickerService>(() => FilePicker());
|
||||
getIt.registerFactory<LocalFileStorage>(() => LocalFileStorage());
|
||||
getIt.registerFactory<ApplicationDataStorage>(() => ApplicationDataStorage());
|
||||
|
||||
getIt.registerFactoryAsync<OpenAIRepository>(
|
||||
() async {
|
||||
|
||||
@@ -20,7 +20,8 @@ abstract class EntryPoint {
|
||||
|
||||
class FlowyRunner {
|
||||
static Future<void> run(
|
||||
EntryPoint f, {
|
||||
EntryPoint f,
|
||||
IntegrationMode mode, {
|
||||
LaunchConfiguration config = const LaunchConfiguration(
|
||||
autoRegistrationSupported: false,
|
||||
),
|
||||
@@ -29,10 +30,9 @@ class FlowyRunner {
|
||||
await getIt.reset();
|
||||
|
||||
// Specify the env
|
||||
final env = integrationEnv();
|
||||
initGetIt(getIt, env, f, config);
|
||||
initGetIt(getIt, mode, f, config);
|
||||
|
||||
final directory = await getIt<LocalFileStorage>()
|
||||
final directory = await getIt<ApplicationDataStorage>()
|
||||
.getPath()
|
||||
.then((value) => Directory(value));
|
||||
|
||||
@@ -55,7 +55,7 @@ class FlowyRunner {
|
||||
|
||||
// init the app widget
|
||||
// ignore in test mode
|
||||
if (!env.isTest()) ...[
|
||||
if (!mode.isTest()) ...[
|
||||
const HotKeyTask(),
|
||||
InitSupabaseTask(
|
||||
url: Env.supabaseUrl,
|
||||
@@ -146,12 +146,17 @@ enum IntegrationMode {
|
||||
develop,
|
||||
release,
|
||||
test,
|
||||
integrationTest,
|
||||
}
|
||||
|
||||
extension IntegrationEnvExt on IntegrationMode {
|
||||
bool isTest() {
|
||||
return this == IntegrationMode.test;
|
||||
}
|
||||
|
||||
bool isIntegrationTest() {
|
||||
return this == IntegrationMode.integrationTest;
|
||||
}
|
||||
}
|
||||
|
||||
IntegrationMode integrationEnv() {
|
||||
|
||||
@@ -21,7 +21,7 @@ class InitRustSDKTask extends LaunchTask {
|
||||
|
||||
@override
|
||||
Future<void> initialize(LaunchContext context) async {
|
||||
final dir = directory ?? await appFlowyDocumentDirectory();
|
||||
final dir = directory ?? await appFlowyApplicationDataDirectory();
|
||||
|
||||
context.getIt<FlowySDK>().setEnv(getAppFlowyEnv());
|
||||
await context.getIt<FlowySDK>().init(dir);
|
||||
@@ -51,7 +51,9 @@ AppFlowyEnv getAppFlowyEnv() {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Directory> appFlowyDocumentDirectory() async {
|
||||
/// The default directory to store the user data. The directory can be
|
||||
/// customized by the user via the [ApplicationDataStorage]
|
||||
Future<Directory> appFlowyApplicationDataDirectory() async {
|
||||
switch (integrationEnv()) {
|
||||
case IntegrationMode.develop:
|
||||
final Directory documentsDir = await getApplicationSupportDirectory()
|
||||
@@ -61,6 +63,7 @@ Future<Directory> appFlowyDocumentDirectory() async {
|
||||
final Directory documentsDir = await getApplicationSupportDirectory();
|
||||
return Directory(path.join(documentsDir.path, 'data')).create();
|
||||
case IntegrationMode.test:
|
||||
case IntegrationMode.integrationTest:
|
||||
return Directory(path.join(Directory.current.path, '.sandbox'));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,10 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
||||
await windowManager.ensureInitialized();
|
||||
windowManager.addListener(this);
|
||||
|
||||
final windowSize = await WindowSizeManager().getSize();
|
||||
Size windowSize = await WindowSizeManager().getSize();
|
||||
if (context.env.isIntegrationTest()) {
|
||||
windowSize = const Size(1600, 1200);
|
||||
}
|
||||
|
||||
final windowOptions = WindowOptions(
|
||||
size: windowSize,
|
||||
|
||||
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user