Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c669f1b85 | ||
|
|
b9976b072e | ||
|
|
409e3bf1e3 | ||
|
|
ff9b3c56c5 | ||
|
|
4c17298432 | ||
|
|
0401fc4b22 | ||
|
|
4805d0c568 | ||
|
|
edc7933c66 | ||
|
|
89ff44b035 | ||
|
|
793884427c | ||
|
|
e0ad364fa3 | ||
|
|
5d76a7864d | ||
|
|
323cb3b60f | ||
|
|
9fb8f221cf | ||
|
|
8dfbfe3c42 | ||
|
|
453be8352a | ||
|
|
452d7eb6d0 | ||
|
|
f0d5f51703 | ||
|
|
2c513be305 | ||
|
|
11d05b303d | ||
|
|
c870dbeac4 | ||
|
|
3acd36e580 | ||
|
|
95d4fb6865 | ||
|
|
30b52a29fd | ||
|
|
a17d6b7eec | ||
|
|
18ed553296 | ||
|
|
7f74fd6149 | ||
|
|
a3e09f6021 | ||
|
|
5e0a0f92ff | ||
|
|
2a2b5fe246 | ||
|
|
f914e7aa33 | ||
|
|
386d3f2ba7 | ||
|
|
88faa0f02a | ||
|
|
f3d6c66f5c | ||
|
|
d665153a9f | ||
|
|
eee32110f4 | ||
|
|
9834eccc7b | ||
|
|
bef7fe87aa | ||
|
|
feceb430cf | ||
|
|
a29c8ab27a | ||
|
|
f1bfcb6066 | ||
|
|
99cfda0a5c | ||
|
|
9bd629aaef | ||
|
|
d302f6b3fb | ||
|
|
5595649f53 | ||
|
|
7de17ab0b8 | ||
|
|
13ff38756f | ||
|
|
197e5b3a79 | ||
|
|
5cced92646 | ||
|
|
e50d708c21 | ||
|
|
79fc7c4cfe | ||
|
|
202cd90711 | ||
|
|
177f7c4fa3 | ||
|
|
d986e01d3e | ||
|
|
8a8915c662 | ||
|
|
efc857d752 | ||
|
|
14dee6b797 | ||
|
|
63eee8b415 | ||
|
|
d96a1d8bd4 |
No files matched your search
@@ -21,12 +21,13 @@ on:
|
||||
- unlocked
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-app:
|
||||
if: github.event.pull_request.draft != true
|
||||
concurrency:
|
||||
group: docker_ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
|
||||
@@ -25,8 +25,13 @@ env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -119,9 +124,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: 20
|
||||
attempt_delay: 10000
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- ".github/workflows/integration_test.yml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
@@ -15,52 +15,70 @@ on:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- ".github/workflows/integration_test.yml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: windows-latest
|
||||
flutter_profile: development-windows-x86
|
||||
target: x86_64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "stable-2022-04-07"
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.10.1"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v2
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
prefix-key: ${{ matrix.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Cache Rust
|
||||
id: cache-rust-target
|
||||
uses: actions/cache@v2
|
||||
- uses: davidB/rust-cargo-make@v1
|
||||
with:
|
||||
path: |
|
||||
frontend/rust-lib/target
|
||||
shared-lib/target
|
||||
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
version: '0.36.6'
|
||||
|
||||
- name: Setup Environment
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
@@ -73,56 +91,50 @@ jobs:
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
|
||||
- if: steps.cache-cargo.outputs.cache-hit != 'true'
|
||||
name: Rust Deps
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
|
||||
- name: Config Flutter
|
||||
- name: Enable Flutter Desktop
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
flutter config --enable-linux-desktop
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter config --enable-macos-desktop
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
git config --system core.longpaths true
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build Test lib
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
cargo make --profile development-linux-x86_64 appflowy-dev
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
cargo make --profile development-mac-x86_64 appflowy-dev
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
cargo make --profile development-windows-x86 appflowy-dev
|
||||
fi
|
||||
shell: bash
|
||||
cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
|
||||
|
||||
- name: Run AppFlowy tests
|
||||
- name: Run Flutter integration tests
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
export DISPLAY=:99
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
sudo apt-get install network-manager
|
||||
flutter test integration_test/runner.dart -d Linux --coverage --verbose
|
||||
flutter test integration_test/runner.dart -d Linux --coverage
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter test integration_test/runner.dart -d macOS --coverage --verbose
|
||||
flutter test integration_test/runner.dart -d macOS --coverage
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter test integration_test/runner.dart -d Windows --coverage --verbose
|
||||
flutter test integration_test/runner.dart -d Windows --coverage
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
- 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: 20
|
||||
attempt_delay: 10000
|
||||
@@ -2,6 +2,7 @@ name: Tauri-CI
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/tauri_ci.yaml"
|
||||
- "frontend/rust-lib/**"
|
||||
- "frontend/appflowy_tauri/**"
|
||||
|
||||
@@ -10,8 +11,13 @@ env:
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tauri-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -38,7 +44,7 @@ jobs:
|
||||
with:
|
||||
prefix-key: 'ubuntu-latest-tauri'
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_tauri/src-tauri
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
|
||||
+52
-1
@@ -1,24 +1,65 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.2.2 - 06/15/2023
|
||||
## Version 0.2.6 - 07/11/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Dynamic load themes
|
||||
- Inline math equation
|
||||
|
||||
|
||||
## Version 0.2.5 - 07/02/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Insert local images
|
||||
- Mention a page
|
||||
- Outlines (Table of contents)
|
||||
- Added support for aligning the image by image menu
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.2.4 - 06/23/2023
|
||||
|
||||
### Bug fixes:
|
||||
|
||||
- Unable to copy and paste a word
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.2.3 - 06/21/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Added support for creating multiple database views for existing database
|
||||
|
||||
## Version 0.2.2 - 06/15/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Added support for embedding a document in the database's row detail page
|
||||
- Added support for inserting an emoji in the database's row detail page
|
||||
|
||||
### Other Updates
|
||||
|
||||
- Added language selector on the welcome page
|
||||
- Added support for importing multiple markdown files all at once
|
||||
|
||||
## Version 0.2.1 - 06/11/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Added support for creating or referencing a calendar in the document
|
||||
- Added `+` icon in grid's add field
|
||||
|
||||
### Other Updates
|
||||
|
||||
- Added vertical padding for progress bar
|
||||
- Hide url cell accessory when the content is empty
|
||||
|
||||
### Bug fixes:
|
||||
|
||||
- Fixed unable to export markdown
|
||||
- Fixed adding vertical padding for progress bar
|
||||
- Fixed database view didn't update after the database layout changed.
|
||||
@@ -26,6 +67,7 @@
|
||||
## Version 0.2.0 - 06/08/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Improved checklists to support each cell having its own list
|
||||
- Drag and drop calendar events
|
||||
- Switch layouts (calendar, grid, kanban) of a database
|
||||
@@ -39,44 +81,53 @@
|
||||
- Added support for an 'Option' button to delete, duplicate, and customize block actions
|
||||
|
||||
### Other Updates
|
||||
|
||||
- Added support for importing v0.1.x documents and databases
|
||||
- Added support for database import and export to CSV
|
||||
- Optimized scroll behavior in documents.
|
||||
- Redesigned the launch page
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fixed bugs related to numbers
|
||||
- Fixed issues with referenced databases in documents
|
||||
- Fixed menu overflow issues in documents
|
||||
|
||||
### Data migration
|
||||
|
||||
The data format of this version is not compatible with previous versions. Therefore, to migrate your data to the new version, you need to use the export and import functions. Please follow the guide to learn how to export and import your data.
|
||||
|
||||
#### Export files in v0.1.6
|
||||
|
||||
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9
|
||||
|
||||
#### Import files in v0.2.0
|
||||
|
||||
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2
|
||||
|
||||
## Version 0.1.5 - 11/05/2023
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: calendar dates don't match with weekdays.
|
||||
- Fix: sort numbers in Grid.
|
||||
|
||||
## Version 0.1.4 - 04/05/2023
|
||||
|
||||
### New features
|
||||
|
||||
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
|
||||
- Writing can be improved with the help of OpenAI.
|
||||
|
||||
## Version 0.1.3 - 24/04/2023
|
||||
|
||||
### New features
|
||||
|
||||
- Launch the official Dark Mode.
|
||||
- Customize the font color and highlight color by setting a hex color value and an opacity level.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: the slash menu can be triggered by all other keyboards than English.
|
||||
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
|
||||
|
||||
|
||||
Vendored
+31
-10
@@ -118,15 +118,11 @@
|
||||
{
|
||||
"label": "AF: Generate Freezed Files",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner build -d",
|
||||
"command": "sh ./scripts/code_generation/freezed/generate_freezed.sh",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/generate_language_files.sh",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"group": "build",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
@@ -134,7 +130,24 @@
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\generate_language_files.cmd"
|
||||
".\\scripts\\code_generation\\freezed\\generate_freezed.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/code_generation/language_files/generate_language_files.sh",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\code_generation\\language_files\\generate_language_files.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -221,5 +234,13 @@
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Env",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs ",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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.6"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -70,5 +70,8 @@ windows/flutter/dart_ffi/
|
||||
**/.vscode/
|
||||
|
||||
*.env
|
||||
*.env.*
|
||||
|
||||
coverage/
|
||||
|
||||
**/failures/*.png
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" style="width: 16px; height: 16px; display: block; fill: inherit; flex-shrink: 0; backface-visibility: hidden;" width="30" height="30" ><path d="M6.04883 27.3232C7.24219 27.3232 7.86426 26.4854 8.20703 25.3936L13.9834 6.79492H27.5039C28.2275 6.79492 28.7227 6.35059 28.7227 5.65234C28.7227 4.97949 28.2275 4.53516 27.5039 4.53516H13.958C12.6631 4.53516 12.0791 5.01758 11.7363 6.13477L6.18848 24.3525H5.97266L3.58594 15.9355C3.38281 15.2373 3.00195 14.9072 2.40527 14.9072C1.73242 14.9072 1.2373 15.3896 1.2373 16.0117C1.2373 16.2656 1.30078 16.4941 1.35156 16.6846L4.04297 25.5332C4.36035 26.5615 4.93164 27.3232 6.04883 27.3232ZM16.3955 24.7334C16.8652 24.7334 17.1064 24.5684 17.4619 24.0732L20.4707 19.8203H20.5215L23.5049 24.0732C23.873 24.5684 24.1143 24.7334 24.5713 24.7334C25.2061 24.7334 25.6758 24.3018 25.6758 23.7051C25.6758 23.4258 25.6123 23.1973 25.4219 22.9561L21.9307 18.208L25.4473 13.4346C25.6377 13.168 25.7139 12.9395 25.7139 12.6855C25.7139 12.127 25.2568 11.6953 24.6475 11.6953C24.2031 11.6953 23.9365 11.8477 23.6064 12.3174L20.7246 16.5957H20.6611L17.6904 12.3047C17.373 11.8477 17.0938 11.6953 16.624 11.6953C16.0146 11.6953 15.5068 12.165 15.5068 12.7363C15.5068 13.0537 15.583 13.2568 15.8115 13.5488L19.1504 18.1445L15.6084 23.0322C15.418 23.2861 15.3672 23.4639 15.3672 23.7559C15.3672 24.3018 15.8115 24.7334 16.3955 24.7334Z" fill="#2F3030"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,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}",,,
|
||||
,,,,,,,
|
||||
@@ -419,4 +419,4 @@
|
||||
"layoutDateField": "تقويم التخطيط بواسطة"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "petit"
|
||||
}
|
||||
@@ -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",
|
||||
@@ -89,7 +90,8 @@
|
||||
"name": "Debug Info",
|
||||
"success": "Copied debug info to clipboard!",
|
||||
"fail": "Unable to copy debug info to clipboard"
|
||||
}
|
||||
},
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"addPageTooltip": "Quickly add a page inside",
|
||||
@@ -184,13 +186,29 @@
|
||||
"open": "Open Settings"
|
||||
},
|
||||
"appearance": {
|
||||
"fontFamily": {
|
||||
"label": "Font Family",
|
||||
"search": "Search"
|
||||
},
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Light Mode",
|
||||
"dark": "Dark Mode",
|
||||
"system": "Adapt to System"
|
||||
},
|
||||
"theme": "Theme"
|
||||
"themeUpload": {
|
||||
"button": "Upload",
|
||||
"description": "Upload your own AppFlowy theme using the button below.",
|
||||
"failure": "The theme that was uploaded had an invalid format.",
|
||||
"loading": "Please wait while we validate and upload your theme...",
|
||||
"uploadSuccess": "Your theme was uploaded successfully",
|
||||
"deletionFailure": "Failed to delete the theme. Try to delete it manually.",
|
||||
"filePickerDialogTitle": "Choose a .flowy_plugin file",
|
||||
"urlUploadFailure": "Failed to open url: {}"
|
||||
},
|
||||
"theme": "Theme",
|
||||
"builtInsLabel": "Built-in Themes",
|
||||
"pluginsLabel": "Plugins"
|
||||
},
|
||||
"files": {
|
||||
"copy": "Copy",
|
||||
@@ -222,6 +240,7 @@
|
||||
"changeLocationTooltips": "Change the data directory",
|
||||
"change": "Change",
|
||||
"openLocationTooltips": "Open another data directory",
|
||||
"openCurrentDataFolder": "Open current data directory",
|
||||
"recoverLocationTooltips": "Reset to AppFlowy's default data directory",
|
||||
"exportFileSuccess": "Export file successfully!",
|
||||
"exportFileFail": "Export file failed!",
|
||||
@@ -235,6 +254,8 @@
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"deleteView": "Are you sure you want to delete this view?",
|
||||
"createView": "New",
|
||||
"settings": {
|
||||
"filter": "Filter",
|
||||
"sort": "Sort",
|
||||
@@ -380,6 +401,9 @@
|
||||
"createANewCalendar": "Create a new Calendar"
|
||||
}
|
||||
},
|
||||
"selectionMenu": {
|
||||
"outline": "Outline"
|
||||
},
|
||||
"plugins": {
|
||||
"referencedBoard": "Referenced Board",
|
||||
"referencedGrid": "Referenced Grid",
|
||||
@@ -402,6 +426,7 @@
|
||||
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
|
||||
"smartEditDisabled": "Connect OpenAI in Settings",
|
||||
"discardResponse": "Do you want to discard the AI responses?",
|
||||
"createInlineMathEquation": "Create equation",
|
||||
"cover": {
|
||||
"changeCover": "Change Cover",
|
||||
"colors": "Colors",
|
||||
@@ -444,6 +469,12 @@
|
||||
"center": "Center",
|
||||
"right": "Right",
|
||||
"defaultColor": "Default"
|
||||
},
|
||||
"image": {
|
||||
"copiedToPasteBoard": "The image link has been copied to the clipboard"
|
||||
},
|
||||
"outline": {
|
||||
"addHeadingToCreateOutline": "Add headings to create a table of contents."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -474,5 +505,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,5 +227,6 @@
|
||||
"column": {
|
||||
"create_new_card": "Nuevo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "pequeño"
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "petit"
|
||||
}
|
||||
@@ -235,5 +235,6 @@
|
||||
"column": {
|
||||
"create_new_card": "Nouveau"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "petit"
|
||||
}
|
||||
@@ -120,14 +120,14 @@
|
||||
"oAuth": {
|
||||
"err": {
|
||||
"failedTitle": "Sikertelen bejelentkezés.",
|
||||
"failedMsg": "Kérjük győződj meg róla, hogy elvégezted a bejelentkezési folyamatot a böngésződben"
|
||||
"failedMsg": "Kérjük győződj meg róla, hogy elvégezted a bejelentkezési folyamatot a böngésződben!"
|
||||
},
|
||||
"google": {
|
||||
"title": "Bejelentkezés Google-al",
|
||||
"title": "Bejelentkezés Google-lal",
|
||||
"instruction1": "Ahhoz, hogy hozzáférj a Google Kontaktjaidhoz, kérjük hatalmazd fel ezt az alkalmazást a böngésződben.",
|
||||
"instruction2": "Másold ezt a kódot a vágólapra az ikonra kattintással vagy a szöveg kijelölésével:",
|
||||
"instruction3": "Nyisd meg ezt a linket a böngésződben, és írjd be a fenti kódot:",
|
||||
"instruction4": "Nyomd meg az alábbi gombot, ha elvégezted a registrációt:"
|
||||
"instruction3": "Nyisd meg ezt a linket a böngésződben, és írd be a fenti kódot:",
|
||||
"instruction4": "Nyomd meg az alábbi gombot, ha elvégezted a regisztrációt:"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -139,15 +139,17 @@
|
||||
},
|
||||
"appearance": {
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"label": "Téma",
|
||||
"light": "Világos mód",
|
||||
"dark": "Éjjeli mód",
|
||||
"system": "Adapt to System"
|
||||
"system": "Rendszerbeállítás követése"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "kicsi"
|
||||
}
|
||||
|
||||
@@ -222,5 +222,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "kecil"
|
||||
}
|
||||
@@ -155,5 +155,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "piccolo"
|
||||
}
|
||||
@@ -203,5 +203,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "小さい"
|
||||
}
|
||||
@@ -231,5 +231,6 @@
|
||||
"column": {
|
||||
"create_new_card": "추가"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "작은"
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "mały"
|
||||
}
|
||||
@@ -145,5 +145,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "pequeno"
|
||||
}
|
||||
@@ -45,7 +45,14 @@
|
||||
"medium": "средний",
|
||||
"large": "большой",
|
||||
"fontSize": "Размер шрифта",
|
||||
"import": "Импортировать"
|
||||
"import": "Импортировать",
|
||||
"moreOptions": ""
|
||||
},
|
||||
"importPanel": {
|
||||
"textAndMarkdown": "Текст и Markdown",
|
||||
"documentFromV010": "Документ из v0.1.0",
|
||||
"databaseFromV010": "База данных из v0.1.0",
|
||||
"csv": "CSV"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Переименовать",
|
||||
@@ -108,8 +115,9 @@
|
||||
"openAsPage": "Открыть как страницу",
|
||||
"addNewRow": "Добавить новую строку",
|
||||
"openMenu": "Открыть меню",
|
||||
"viewDataBase": "Просмотр базы данных",
|
||||
"referencePage": "Ссылаются на {name}"
|
||||
"dragRow": "Долгое нажатие для изменения порядка строк",
|
||||
"viewDataBase": "Открыть базу данных",
|
||||
"referencePage": "Ссылки на {name}"
|
||||
},
|
||||
"sideBar": {
|
||||
"closeSidebar": "Закрыть боковое меню",
|
||||
@@ -123,7 +131,7 @@
|
||||
},
|
||||
"contactsPage": {
|
||||
"title": "Контакты",
|
||||
"whatsHappening": "Какие события на этой неделе?",
|
||||
"whatsHappening": "Что нового на этой неделе?",
|
||||
"addContact": "Добавить контакт",
|
||||
"editContact": "Редактировать контакт"
|
||||
},
|
||||
@@ -182,15 +190,20 @@
|
||||
"theme": "Тема"
|
||||
},
|
||||
"files": {
|
||||
"copy": "Копировать",
|
||||
"defaultLocation": "Где сейчас хранятся ваши данные",
|
||||
"exportData": "Экспорт данных",
|
||||
"doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь",
|
||||
"restoreLocation": "Восстановить путь AppFlowy по умолчанию",
|
||||
"customizeLocation": "Открыть другую папку",
|
||||
"restartApp": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.",
|
||||
"exportDatabase": "Экспорт базы данных",
|
||||
"selectFiles": "Выбрать файлы, которые необходимо экспортировать",
|
||||
"selectAll": "Выбрать всё",
|
||||
"deselectAll": "Снять выделение",
|
||||
"createNewFolder": "Создать новую папку",
|
||||
"createNewFolderDesc": "Указать, где хранить свои данные ...",
|
||||
"defineWhereYourDataIsStored": "Указать хранилище данных",
|
||||
"open": "Открыть",
|
||||
"openFolder": "Открыть существующую папку",
|
||||
"openFolderDesc": "Чтение и запись в существующую папку AppFlowy ...",
|
||||
@@ -199,9 +212,17 @@
|
||||
"locationDesc": "Выбрать имя папки данных AppFlowy",
|
||||
"browser": "Обзор",
|
||||
"create": "Создать",
|
||||
"set": "Set",
|
||||
"folderPath": "Путь к вашей папке",
|
||||
"locationCannotBeEmpty": "Путь не может быть пустым",
|
||||
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!"
|
||||
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!",
|
||||
"changeLocationTooltips": "Сменить местоположения",
|
||||
"change": "Изменить",
|
||||
"openLocationTooltips": "Открыть другую директорию данных",
|
||||
"recoverLocationTooltips": "Сбросить к местоположению по умолчанию",
|
||||
"exportFileSuccess": "Экспорт завершён!",
|
||||
"exportFileFail": "Не удалость экспортировать!",
|
||||
"export": "Экспорт"
|
||||
},
|
||||
"user": {
|
||||
"name": "Имя",
|
||||
@@ -221,7 +242,8 @@
|
||||
"deleteFilter": "Удалить фильтр",
|
||||
"filterBy": "Фильтровать по...",
|
||||
"typeAValue": "Введите значение...",
|
||||
"layout": "Раскладка"
|
||||
"layout": "Вид",
|
||||
"databaseLayout": "Вид базы данных"
|
||||
},
|
||||
"textFilter": {
|
||||
"contains": "Содержит",
|
||||
@@ -272,6 +294,8 @@
|
||||
"textFieldName": "Текст",
|
||||
"checkboxFieldName": "Чекбокс",
|
||||
"dateFieldName": "Дата",
|
||||
"updatedAtFieldName": "Последнее изменение",
|
||||
"createdAtFieldName": "Создано",
|
||||
"numberFieldName": "Число",
|
||||
"singleSelectFieldName": "Выбор",
|
||||
"multiSelectFieldName": "Выбор нескольких",
|
||||
@@ -341,12 +365,16 @@
|
||||
},
|
||||
"slashMenu": {
|
||||
"board": {
|
||||
"selectABoardToLinkTo": "Выбрать доску для связи с",
|
||||
"selectABoardToLinkTo": "Выбрать доску",
|
||||
"createANewBoard": "Создать доску"
|
||||
},
|
||||
"grid": {
|
||||
"selectAGridToLinkTo": "Выберите сетку для связи с",
|
||||
"selectAGridToLinkTo": "Выберите сетку",
|
||||
"createANewGrid": "Создать сетку"
|
||||
},
|
||||
"calendar": {
|
||||
"selectACalendarToLinkTo": "Выбрать календарь",
|
||||
"createANewCalendar": "Создать календарь"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
@@ -363,6 +391,8 @@
|
||||
"smartEditFixSpelling": "Исправить правописание",
|
||||
"warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.",
|
||||
"smartEditSummarize": "Выделить суть",
|
||||
"smartEditImproveWriting": "Улучшить",
|
||||
"smartEditMakeLonger": "Продолжить",
|
||||
"smartEditCouldNotFetchResult": "Не могу получить ответ от OpenAI",
|
||||
"smartEditCouldNotFetchKey": "Не могу получить токен OpenAI",
|
||||
"smartEditDisabled": "Подключить OpenAI",
|
||||
@@ -394,6 +424,21 @@
|
||||
"mathEquation": {
|
||||
"addMathEquation": "Добавить математическое выражение",
|
||||
"editMathEquation": "Редактировать математическое выражение"
|
||||
},
|
||||
"optionAction": {
|
||||
"click": "Кликните",
|
||||
"toOpenMenu": " чтобы открыть меню",
|
||||
"delete": "Удалить",
|
||||
"duplicate": "Дублировать",
|
||||
"turnInto": "Превратить",
|
||||
"moveUp": "Поднять",
|
||||
"moveDown": "Опустить",
|
||||
"color": "Цвет",
|
||||
"align": "Выравнивание",
|
||||
"left": "По левому краю",
|
||||
"center": "По центру",
|
||||
"right": "По правому краю",
|
||||
"defaultColor": "Цвет по умолчанию"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -417,7 +462,12 @@
|
||||
"showWeekNumbers": "Показывать номера недель",
|
||||
"showWeekends": "Показывать выходные",
|
||||
"firstDayOfWeek": "Первый день недели",
|
||||
"layoutDateField": "Вид календаря"
|
||||
}
|
||||
"layoutDateField": "Вид календаря",
|
||||
"noDateTitle": "No Date",
|
||||
"noDateHint": "Unscheduled events will show up here",
|
||||
"clickToAdd": "Click to add to the calendar",
|
||||
"name": "Calendar layout"
|
||||
},
|
||||
"referencedCalendarPrefix": "Вид"
|
||||
}
|
||||
}
|
||||
@@ -235,5 +235,6 @@
|
||||
"column": {
|
||||
"create_new_card": "Nytt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"moreAction.small": "små"
|
||||
}
|
||||
@@ -149,5 +149,6 @@
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
},
|
||||
"moreAction.small": "küçük"
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
"subscribeNewsletterText": "消息订阅",
|
||||
"letsGoButtonText": "开始",
|
||||
"title": "标题",
|
||||
"youCanAlso": "您还可以",
|
||||
"and": "以及",
|
||||
"signUp": {
|
||||
"buttonText": "注册",
|
||||
"title": "注册 @:appName 账户",
|
||||
@@ -21,6 +23,7 @@
|
||||
"signIn": {
|
||||
"loginTitle": "登录 @:appName",
|
||||
"loginButtonText": "登录",
|
||||
"loginAsGuestButtonText": "开始使用",
|
||||
"buttonText": "登录",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"emailHint": "邮箱",
|
||||
@@ -40,6 +43,21 @@
|
||||
"markdown": "Markdown",
|
||||
"copyLink": "复制链接"
|
||||
},
|
||||
"moreAction": {
|
||||
"small": "小",
|
||||
"medium": "中",
|
||||
"large": "大",
|
||||
"fontSize": "字号",
|
||||
"import": "导入",
|
||||
"moreOptions": "更多选项"
|
||||
},
|
||||
"importPanel": {
|
||||
"textAndMarkdown": "文本 & Markdown",
|
||||
"documentFromV010": "来自 v0.1.0 的文档",
|
||||
"databaseFromV010": "来自 v0.1.0 的数据库",
|
||||
"csv": "CSV",
|
||||
"database": "数据库"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
@@ -54,7 +72,7 @@
|
||||
"pageHeader": {
|
||||
"fileName": "文件名",
|
||||
"lastModified": "最近修改",
|
||||
"created": "创建"
|
||||
"created": "创建时间"
|
||||
}
|
||||
},
|
||||
"deletePagePrompt": {
|
||||
@@ -64,8 +82,10 @@
|
||||
},
|
||||
"dialogCreatePageNameHint": "页面名称",
|
||||
"questionBubble": {
|
||||
"shortcuts": "快捷键",
|
||||
"whatsNew": "新功能?",
|
||||
"help": "帮助 & 支持",
|
||||
"markdown": "Markdown",
|
||||
"debug": {
|
||||
"name": "调试信息",
|
||||
"success": "将调试信息复制到剪贴板!",
|
||||
@@ -90,14 +110,18 @@
|
||||
"inlineCode": "内联代码",
|
||||
"quote": "块引用",
|
||||
"header": "标题",
|
||||
"highlight": "高亮"
|
||||
"highlight": "高亮",
|
||||
"color": "颜色"
|
||||
},
|
||||
"tooltip": {
|
||||
"lightMode": "切换到亮色模式",
|
||||
"darkMode": "切换到暗色模式",
|
||||
"openAsPage": "作为页面打开",
|
||||
"addNewRow": "增加一行",
|
||||
"openMenu": "点击打开菜单"
|
||||
"addNewRow": "添加一行",
|
||||
"openMenu": "点击打开菜单",
|
||||
"dragRow": "拖动来调整行",
|
||||
"viewDataBase": "查看数据库",
|
||||
"referencePage": "这个 {name} 正在被引用"
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "打开侧边栏",
|
||||
@@ -117,11 +141,19 @@
|
||||
},
|
||||
"button": {
|
||||
"OK": "确认",
|
||||
"Done": "完成",
|
||||
"Cancel": "取消",
|
||||
"signIn": "登录",
|
||||
"signOut": "登出",
|
||||
"complete": "完成",
|
||||
"save": "保存"
|
||||
"save": "保存",
|
||||
"generate": "生成",
|
||||
"esc": "ESC",
|
||||
"keep": "保留",
|
||||
"tryAgain": "重试",
|
||||
"discard": "放弃",
|
||||
"replace": "替换",
|
||||
"insertBelow": "下方插入"
|
||||
},
|
||||
"label": {
|
||||
"welcome": "欢迎!",
|
||||
@@ -149,37 +181,133 @@
|
||||
"appearance": "外观",
|
||||
"language": "语言",
|
||||
"user": "用户",
|
||||
"files": "文件",
|
||||
"open": "打开设置"
|
||||
},
|
||||
"appearance": {
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"label": "主题模式",
|
||||
"light": "日间模式",
|
||||
"dark": "夜间模式",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
"system": "系统自适应"
|
||||
},
|
||||
"theme": "主题"
|
||||
},
|
||||
"files": {
|
||||
"copy": "复制",
|
||||
"defaultLocation": "读取文件和数据存储位置",
|
||||
"exportData": "导出您的数据",
|
||||
"doubleTapToCopy": "双击复制路径",
|
||||
"restoreLocation": "恢复为 AppFlowy 默认路径",
|
||||
"customizeLocation": "打开另一个文件夹",
|
||||
"restartApp": "请重启 app 使设置生效",
|
||||
"exportDatabase": "导出数据库",
|
||||
"selectFiles": "选择需要导出的文件",
|
||||
"selectAll": "全选",
|
||||
"deselectAll": "取消全选",
|
||||
"createNewFolder": "新建文件夹",
|
||||
"createNewFolderDesc": "告诉我们您要将数据存储在何处",
|
||||
"defineWhereYourDataIsStored": "定义数据存储位置",
|
||||
"open": "打开",
|
||||
"openFolder": "打开现有文件夹",
|
||||
"openFolderDesc": "读取并将其写入您现有的 AppFlowy 文件夹",
|
||||
"folderHintText": "文件夹名",
|
||||
"location": "正在新建文件夹",
|
||||
"locationDesc": "为您的 AppFlowy 数据文件夹选择一个名称",
|
||||
"browser": "浏览",
|
||||
"create": "新建",
|
||||
"set": "设置",
|
||||
"folderPath": "保存文件夹的路径",
|
||||
"locationCannotBeEmpty": "路径不能为空",
|
||||
"pathCopiedSnackbar": "文件存储路径已被复制到剪贴板!",
|
||||
"changeLocationTooltips": "更改数据目录",
|
||||
"change": "更改",
|
||||
"openLocationTooltips": "打开另一个数据目录",
|
||||
"openCurrentDataFolder": "打开当前数据目录",
|
||||
"recoverLocationTooltips": "恢复为 AppFlowy 默认数据目录",
|
||||
"exportFileSuccess": "导出文件成功!",
|
||||
"exportFileFail": "导出文件失败!",
|
||||
"export": "导出"
|
||||
},
|
||||
"user": {
|
||||
"name": "名字",
|
||||
"icon": "图标",
|
||||
"selectAnIcon": "选择一个图标",
|
||||
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"deleteView": "您确定要删除这个视窗吗?",
|
||||
"createView": "新建",
|
||||
"settings": {
|
||||
"filter": "过滤器",
|
||||
"filter": "筛选",
|
||||
"sort": "以……排序",
|
||||
"sortBy": "排序",
|
||||
"Properties": "属性",
|
||||
"group": "组"
|
||||
"group": "组",
|
||||
"addFilter": "添加筛选",
|
||||
"deleteFilter": "删除筛选",
|
||||
"filterBy": "以……筛选",
|
||||
"typeAValue": "请输入一个值",
|
||||
"layout": "布局",
|
||||
"databaseLayout": "布局"
|
||||
},
|
||||
"textFilter": {
|
||||
"contains": "包含",
|
||||
"doesNotContain": "不包含",
|
||||
"endsWith": "以……结束",
|
||||
"startWith": "以……开始",
|
||||
"is": "等于",
|
||||
"isNot": "不等于",
|
||||
"isEmpty": "为空",
|
||||
"isNotEmpty": "不为空",
|
||||
"choicechipPrefix": {
|
||||
"isNot": "不等于",
|
||||
"startWith": "以……开始",
|
||||
"endWith": "以……结束",
|
||||
"isEmpty": "为空",
|
||||
"isNotEmpty": "不为空"
|
||||
}
|
||||
},
|
||||
"checkboxFilter": {
|
||||
"isChecked": "已勾选",
|
||||
"isUnchecked": "未勾选",
|
||||
"choicechipPrefix": {
|
||||
"is": "is"
|
||||
}
|
||||
},
|
||||
"checklistFilter": {
|
||||
"isComplete": "已完成",
|
||||
"isIncomplted": "未完成"
|
||||
},
|
||||
"singleSelectOptionFilter": {
|
||||
"is": "等于",
|
||||
"isNot": "不等于",
|
||||
"isEmpty": "为空",
|
||||
"isNotEmpty": "不为空"
|
||||
},
|
||||
"multiSelectOptionFilter": {
|
||||
"contains": "包含",
|
||||
"doesNotContain": "不包含",
|
||||
"isEmpty": "为空",
|
||||
"isNotEmpty": "不为空"
|
||||
},
|
||||
"field": {
|
||||
"hide": "隐藏",
|
||||
"insertLeft": "左侧插入",
|
||||
"insertRight": "右侧插入",
|
||||
"duplicate": "拷贝",
|
||||
"duplicate": "复制",
|
||||
"delete": "删除",
|
||||
"textFieldName": "文本",
|
||||
"checkboxFieldName": "勾选框",
|
||||
"dateFieldName": "日期",
|
||||
"updatedAtFieldName": "修改时间",
|
||||
"createdAtFieldName": "创建时间",
|
||||
"numberFieldName": "数字",
|
||||
"singleSelectFieldName": "单项选择器",
|
||||
"multiSelectFieldName": "多项选择器",
|
||||
"urlFieldName": "链接",
|
||||
"checklistFieldName": "清单",
|
||||
"numberFormat": "数字格式",
|
||||
"dateFormat": "日期格式",
|
||||
"includeTime": "包含时间",
|
||||
@@ -187,6 +315,7 @@
|
||||
"dateFormatISO": "年-月-日",
|
||||
"dateFormatLocal": "月/日/年",
|
||||
"dateFormatUS": "年/月/日",
|
||||
"dateFormatDayMonthYear": "日/月/年",
|
||||
"timeFormat": "时间格式",
|
||||
"invalidTimeFormat": "时间格式错误",
|
||||
"timeFormatTwelveHour": "12小时制",
|
||||
@@ -195,16 +324,23 @@
|
||||
"optionTitle": "标签",
|
||||
"addOption": "添加标签",
|
||||
"editProperty": "编辑列属性",
|
||||
"newProperty": "增加一列",
|
||||
"newProperty": "添加一列",
|
||||
"deleteFieldPromptMessage": "确定要删除这个属性吗? "
|
||||
},
|
||||
"sort": {
|
||||
"ascending": "升序",
|
||||
"descending": "降序",
|
||||
"deleteSort": "删除排序",
|
||||
"addSort": "添加排序"
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "复制",
|
||||
"delete": "删除",
|
||||
"textPlaceholder": "空",
|
||||
"copyProperty": "复制列",
|
||||
"count": "数量",
|
||||
"newRow": "添加一行"
|
||||
"newRow": "添加一行",
|
||||
"action": "执行"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "新建",
|
||||
@@ -222,19 +358,132 @@
|
||||
"panelTitle": "选择或新建一个标签",
|
||||
"searchOption": "搜索标签"
|
||||
},
|
||||
"menuName": "网格"
|
||||
"checklist": {
|
||||
"panelTitle": "添加项"
|
||||
},
|
||||
"menuName": "网格",
|
||||
"referencedGridPrefix": "视图"
|
||||
},
|
||||
"document": {
|
||||
"menuName": "文档",
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
},
|
||||
"slashMenu": {
|
||||
"board": {
|
||||
"selectABoardToLinkTo": "选择要链接到的看板",
|
||||
"createANewBoard": "新建看板"
|
||||
},
|
||||
"grid": {
|
||||
"selectAGridToLinkTo": "选择要链接到的网格",
|
||||
"createANewGrid": "新建网格"
|
||||
},
|
||||
"calendar": {
|
||||
"selectACalendarToLinkTo": "选择要链接到的日历",
|
||||
"createANewCalendar": "新建日历"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"referencedBoard": "引用的看板",
|
||||
"referencedGrid": "引用的网格",
|
||||
"referencedCalendar": "引用的日历",
|
||||
"autoGeneratorMenuItemName": "OpenAI 创作",
|
||||
"autoGeneratorTitleName": "OpenAI: 让 AI 写些什么...",
|
||||
"autoGeneratorLearnMore": "学习更多",
|
||||
"autoGeneratorGenerate": "生成",
|
||||
"autoGeneratorHintText": "让 OpenAI ...",
|
||||
"autoGeneratorCantGetOpenAIKey": "无法获得 OpenAI 密钥",
|
||||
"autoGeneratorRewrite": "重写",
|
||||
"smartEdit": "AI 助手",
|
||||
"openAI": "OpenAI",
|
||||
"smartEditFixSpelling": "修正拼写",
|
||||
"warning": "⚠️ AI 可能不准确或具有误导性.",
|
||||
"smartEditSummarize": "总结",
|
||||
"smartEditImproveWriting": "提高写作水平",
|
||||
"smartEditMakeLonger": "丰富内容",
|
||||
"smartEditCouldNotFetchResult": "无法从 OpenAI 获取到结果",
|
||||
"smartEditCouldNotFetchKey": "无法获取到 OpenAI 密钥",
|
||||
"smartEditDisabled": "在设置中连接 OpenAI",
|
||||
"discardResponse": "您是否要放弃 AI 继续写作?",
|
||||
"cover": {
|
||||
"changeCover": "修改封面",
|
||||
"colors": "颜色",
|
||||
"images": "图像",
|
||||
"clearAll": "清除所有",
|
||||
"abstract": "摘要",
|
||||
"addCover": "添加封面",
|
||||
"addLocalImage": "添加本地图像",
|
||||
"invalidImageUrl": "无效的图像网址",
|
||||
"failedToAddImageToGallery": "无法将图片添加到库",
|
||||
"enterImageUrl": "输入图像网址",
|
||||
"add": "添加",
|
||||
"back": "返回",
|
||||
"saveToGallery": "保存至库",
|
||||
"removeIcon": "移除图标",
|
||||
"pasteImageUrl": "粘贴图片网址",
|
||||
"or": "或",
|
||||
"pickFromFiles": "从文件中选取",
|
||||
"couldNotFetchImage": "无法获取到图像",
|
||||
"imageSavingFailed": "图像保存失败",
|
||||
"addIcon": "添加图标",
|
||||
"coverRemoveAlert": "删除后将从封面中移除。",
|
||||
"alertDialogConfirmation": "您确定您要继续吗?"
|
||||
},
|
||||
"mathEquation": {
|
||||
"addMathEquation": "添加数学公式",
|
||||
"editMathEquation": "编辑数学公式"
|
||||
},
|
||||
"optionAction": {
|
||||
"click": "点击",
|
||||
"toOpenMenu": " 来打开菜单",
|
||||
"delete": "删除",
|
||||
"duplicate": "复制",
|
||||
"turnInto": "变成",
|
||||
"moveUp": "上移",
|
||||
"moveDown": "下移",
|
||||
"color": "颜色",
|
||||
"align": "对齐",
|
||||
"left": "左",
|
||||
"center": "中心",
|
||||
"right": "又",
|
||||
"defaultColor": "默认"
|
||||
}
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"column": {
|
||||
"create_new_card": "新建"
|
||||
},
|
||||
"menuName": "看板"
|
||||
"menuName": "看板",
|
||||
"referencedBoardPrefix": "视图"
|
||||
},
|
||||
"moreAction.small": "小的",
|
||||
"calendar": {
|
||||
"menuName": "日历",
|
||||
"defaultNewCalendarTitle": "未命名",
|
||||
"navigation": {
|
||||
"today": "今天",
|
||||
"jumpToday": "跳转到今天",
|
||||
"previousMonth": "上一月",
|
||||
"nextMonth": "下一月"
|
||||
},
|
||||
"settings": {
|
||||
"showWeekNumbers": "显示周数",
|
||||
"showWeekends": "显示周末",
|
||||
"firstDayOfWeek": "一周开始于",
|
||||
"layoutDateField": "以……为日历布局",
|
||||
"noDateTitle": "没有日期",
|
||||
"noDateHint": "计划外事件将显示在此处",
|
||||
"clickToAdd": "单击以添加到日历",
|
||||
"name": "日历布局"
|
||||
},
|
||||
"referencedCalendarPrefix": "视图"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "AppFlowy 错误",
|
||||
"howToFixFallback": "对于给您带来的不便, 我们深表歉意! 请在我们的 GitHub 页面上提交 issue 并描述您遇到的错误。",
|
||||
"github": "在 GitHub 查看"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('cover image', () {
|
||||
const location = 'cover_image';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'hovering on cover image will display change and delete cover image buttons',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
await tester.editor.hoverOnCoverPluginAddButton();
|
||||
|
||||
tester.expectToSeePluginAddCoverAndIconButton();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('calendar database view', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('calendar start from day setting', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create calendar view
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar');
|
||||
|
||||
// Open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapCalendarLayoutSettingButton();
|
||||
|
||||
// select the first day of week is Monday
|
||||
await tester.tapFirstDayOfWeek();
|
||||
await tester.tapFirstDayOfWeekStartFromMonday();
|
||||
|
||||
// Open the other page and open the new calendar page again
|
||||
await tester.openPage(readme);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
await tester.openPage('calendar');
|
||||
|
||||
// Open setting again and check the start from Monday is selected
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapCalendarLayoutSettingButton();
|
||||
await tester.tapFirstDayOfWeek();
|
||||
tester.assertFirstDayOfWeekStartFromMonday();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('creating and editing calendar events', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Scroll until today's date cell is visible
|
||||
await tester.scrollToToday();
|
||||
|
||||
// Hover over today's calendar cell
|
||||
await tester.hoverOnTodayCalendarCell();
|
||||
|
||||
// Tap on create new event button
|
||||
await tester.tapAddCalendarEventButton();
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
// Double click on today's calendar cell to create a new event
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is inserted in the cell
|
||||
tester.assertNumberOfEventsInCalendar(2);
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Change the title of the event
|
||||
await tester.editTitleInRowDetailPage('hello world');
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Duplicate the event
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Check that there are 2 events
|
||||
tester.assertNumberOfEventsInCalendar(2, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(3, DateTime.now());
|
||||
|
||||
// Delete an event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Check that there is 1 event
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
});
|
||||
|
||||
testWidgets('rescheduling events', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Create a new event on the first of this month
|
||||
final today = DateTime.now();
|
||||
final firstOfThisMonth = DateTime(today.year, today.month, 1);
|
||||
await tester.doubleClickCalendarCell(firstOfThisMonth);
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Drag and drop the event onto the next week, same day
|
||||
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
|
||||
|
||||
// Make sure that the event has been rescheduled to the new date
|
||||
final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, sameDayNextWeek);
|
||||
|
||||
// Delete the event
|
||||
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
await tester.scrollToToday();
|
||||
await tester.doubleClickCalendarCell(today);
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is today
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, today);
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
|
||||
// Open the date editor of the event
|
||||
await tester.tapDateCellInRowDetailPage();
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Edit the event's date. To avoid selecting a day outside of the current month, the new date will be one day closer to the middle of the month.
|
||||
final newDate = today.day < 15
|
||||
? today.add(const Duration(days: 1))
|
||||
: today.subtract(const Duration(days: 1));
|
||||
await tester.selectDay(content: newDate.day);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, newDate);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
@@ -9,21 +11,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid cell', () {
|
||||
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('edit text cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -46,6 +33,44 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
// Makesure the text cells are filled with the right content when there are
|
||||
// multiple text cell
|
||||
testWidgets('edit multiple text cells', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid');
|
||||
await tester.createField(FieldType.RichText, 'description');
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
input: 'hello',
|
||||
);
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
input: 'world',
|
||||
cellIndex: 1,
|
||||
);
|
||||
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
content: 'hello',
|
||||
cellIndex: 0,
|
||||
);
|
||||
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
content: 'world',
|
||||
cellIndex: 1,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit number cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -176,8 +201,235 @@ void main() {
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Select the date
|
||||
await tester.selectDay(content: 3);
|
||||
// Toggle include time
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
// Dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Turn off include time
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
// Select a date
|
||||
final today = DateTime.now();
|
||||
await tester.selectDay(content: today.day);
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM dd, y').format(today),
|
||||
);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Toggle include time
|
||||
final now = DateTime.now();
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM dd, y HH:mm').format(now),
|
||||
);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Change date format
|
||||
await tester.changeDateFormat();
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y HH:mm').format(now),
|
||||
);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Change time format
|
||||
await tester.changeTimeFormat();
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y hh:mm a').format(now),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit single select cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
const fieldType = FieldType.SingleSelect;
|
||||
await tester.tapAddButton();
|
||||
// When create a grid, it will create a single select field by default
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Tap the cell to invoke the selection option editor
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Create a new select option
|
||||
await tester.createOption(name: 'tag 1');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Make sure the option is created and displayed in the cell
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'tag 1',
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Create another select option
|
||||
await tester.createOption(name: 'tag 2');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'tag 2',
|
||||
);
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// switch to first option
|
||||
await tester.selectOption(name: 'tag 1');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'tag 1',
|
||||
);
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Deselect the currently-selected option
|
||||
await tester.selectOption(name: 'tag 1');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNothing,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit multi select cell', (tester) async {
|
||||
final tags = [
|
||||
'tag 1',
|
||||
'tag 2',
|
||||
'tag 3',
|
||||
'tag 4',
|
||||
];
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
const fieldType = FieldType.MultiSelect;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
// Tap the cell to invoke the selection option editor
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Create a new select option
|
||||
await tester.createOption(name: tags.first);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Make sure the option is created and displayed in the cell
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tags.first,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Create some other select options
|
||||
await tester.createOption(name: tags[1]);
|
||||
await tester.createOption(name: tags[2]);
|
||||
await tester.createOption(name: tags[3]);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
for (final tag in tags) {
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tag,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNWidgets(4),
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Deselect all options
|
||||
for (final tag in tags) {
|
||||
await tester.selectOption(name: tag);
|
||||
}
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNothing,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Select some options
|
||||
await tester.selectOption(name: tags[1]);
|
||||
await tester.selectOption(name: tags[3]);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tags[1],
|
||||
);
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tags[3],
|
||||
);
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNWidgets(2),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
@@ -10,21 +10,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid page', () {
|
||||
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('rename existing field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -114,7 +99,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,142 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
testWidgets('add text filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name');
|
||||
await tester.tapFilterButtonInGrid('Name');
|
||||
|
||||
// enter 'A' in the filter text field
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
await tester.enterTextInTextFilter('A');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// after remove the filter, the grid should show all rows
|
||||
await tester.enterTextInTextFilter('');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.enterTextInTextFilter('B');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// open the menu to delete the filter
|
||||
await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checkbox filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Done');
|
||||
await tester.tapCheckboxFilterButtonInGrid();
|
||||
|
||||
await tester.tapUnCheckedButtonOnCheckboxFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester
|
||||
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checklist filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');
|
||||
|
||||
// By default, the condition of checklist filter is 'uncompleted'
|
||||
await tester.assertNumberOfRowsInGridPage(9);
|
||||
|
||||
await tester.tapFilterButtonInGrid('checklist');
|
||||
await tester.tapChecklistFilterButtonInGrid();
|
||||
|
||||
await tester.tapCompletedButtonOnChecklistFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add single select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type');
|
||||
|
||||
await tester.tapFilterButtonInGrid('Type');
|
||||
|
||||
// select the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
// unselect the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
// select the option 's5'
|
||||
await tester.tapOptionFilterWithName('s5');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// select the option 's4'
|
||||
await tester.tapOptionFilterWithName('s4');
|
||||
|
||||
// The row with 's4' or 's5' should be shown.
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add multi select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.MultiSelect,
|
||||
'multi-select',
|
||||
);
|
||||
|
||||
await tester.tapFilterButtonInGrid('multi-select');
|
||||
await tester.scrollOptionFilterListByOffset(const Offset(0, -200));
|
||||
|
||||
// select the option 'm1'. Any option with 'm1' should be shown.
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
|
||||
// select the option 'm2'. Any option with 'm2' should be shown.
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
|
||||
// select the option 'm4'. Any option with 'm4' should be shown.
|
||||
await tester.tapOptionFilterWithName('m4');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/emoji.dart';
|
||||
import 'util/ime.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -13,22 +14,7 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
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('open first row of the grid', (tester) async {
|
||||
testWidgets('row details page opens', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -38,6 +24,9 @@ void main() {
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Make sure that the row page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
});
|
||||
|
||||
testWidgets('insert emoji in the row detail page', (tester) async {
|
||||
@@ -83,7 +72,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 +98,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);
|
||||
});
|
||||
@@ -183,9 +172,10 @@ void main() {
|
||||
// Focus on the editor
|
||||
final textBlock = find.byType(TextBlockComponentWidget);
|
||||
await tester.tapAt(tester.getCenter(textBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Input some text
|
||||
const inputText = 'Hello world';
|
||||
const inputText = 'Hello World';
|
||||
await tester.ime.insertText(inputText);
|
||||
expect(
|
||||
find.textContaining(inputText, findRichText: true),
|
||||
|
||||
@@ -8,21 +8,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
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 row of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -9,21 +9,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
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 layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
testWidgets('import v0.2.0 database data', (tester) async {
|
||||
await tester.openV020database();
|
||||
// wait the database data is loaded
|
||||
await tester.pumpAndSettle(const Duration(microseconds: 500));
|
||||
|
||||
// check the text cell
|
||||
final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];
|
||||
for (final (index, content) in textCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the checkbox cell
|
||||
final checkboxCells = <bool>[
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
];
|
||||
for (final (index, content) in checkboxCells.indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the number cell
|
||||
final numberCells = <String>[
|
||||
'-1',
|
||||
'-2',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
'2',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
''
|
||||
];
|
||||
for (final (index, content) in numberCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the url cell
|
||||
final urlCells = <String>[
|
||||
'appflowy.io',
|
||||
'no url',
|
||||
'appflowy.io',
|
||||
'https://github.com/AppFlowy-IO/',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in urlCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.URL,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the single select cell
|
||||
final singleSelectCells = <String>[
|
||||
's1',
|
||||
's2',
|
||||
's3',
|
||||
's4',
|
||||
's5',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in singleSelectCells.indexed) {
|
||||
await tester.assertSingleSelectOption(
|
||||
rowIndex: index,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the multi select cell
|
||||
final List<List<String>> multiSelectCells = [
|
||||
['m1'],
|
||||
['m1', 'm2'],
|
||||
['m1', 'm2', 'm3'],
|
||||
['m1', 'm2', 'm3'],
|
||||
['m1', 'm2', 'm3', 'm4', 'm5'],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
];
|
||||
for (final (index, contents) in multiSelectCells.indexed) {
|
||||
await tester.assertMultiSelectOption(
|
||||
rowIndex: index,
|
||||
contents: contents,
|
||||
);
|
||||
}
|
||||
|
||||
// check the checklist cell
|
||||
final List<double> checklistCells = [
|
||||
0.6,
|
||||
0.3,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
];
|
||||
for (final (index, percent) in checklistCells.indexed) {
|
||||
await tester.assertChecklistCellInGrid(
|
||||
rowIndex: index,
|
||||
percent: percent,
|
||||
);
|
||||
}
|
||||
|
||||
// check the date cell
|
||||
final List<String> dateCells = [
|
||||
'Jun 01, 2023',
|
||||
'Jun 02, 2023',
|
||||
'Jun 03, 2023',
|
||||
'Jun 04, 2023',
|
||||
'Jun 05, 2023',
|
||||
'Jun 05, 2023',
|
||||
'Jun 16, 2023',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
];
|
||||
for (final (index, content) in dateCells.indexed) {
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.DateTime,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
testWidgets('add text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
// check the text cell order
|
||||
final textCells = <String>[
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
];
|
||||
for (final (index, content) in textCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('Name');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'E',
|
||||
'D',
|
||||
'C',
|
||||
'B',
|
||||
'A',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// delete all sorts
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapAllSortButton();
|
||||
|
||||
// check the text cell order
|
||||
for (final (index, content) in <String>[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checkbox sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
|
||||
// check the checkbox cell order
|
||||
for (final (index, content) in <bool>[
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('Done');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <bool>[
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add number sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
|
||||
// check the number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'',
|
||||
'-2',
|
||||
'-1',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
'2',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('number');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add number and text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
|
||||
// open the sort menu and select number order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('number');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapCreateSortByFieldTypeInSortMenu(
|
||||
FieldType.RichText,
|
||||
'Name',
|
||||
);
|
||||
|
||||
// check number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'',
|
||||
'-1',
|
||||
'-2',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check text cell order
|
||||
for (final (index, content) in <String>[
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
testWidgets('create linked view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// Create grid view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
|
||||
|
||||
// Create calendar view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('rename and delete linked view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// rename board view
|
||||
await tester.renameLinkedView(
|
||||
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
|
||||
'new board',
|
||||
);
|
||||
final findBoard = tester.findTabBarLinkViewByViewName('new board');
|
||||
expect(findBoard, findsOneWidget);
|
||||
|
||||
// delete the board
|
||||
await tester.deleteDatebaseView(findBoard);
|
||||
expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('delete the last database view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// delete the board
|
||||
await tester.deleteDatebaseView(
|
||||
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-17
@@ -4,27 +4,12 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
group('create and delete the document', () {
|
||||
testWidgets('create a new document when launching app in first time',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_create_and_delete_test.dart'
|
||||
as document_create_and_delete_test;
|
||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
|
||||
void startTesting() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_create_and_delete_test.main();
|
||||
document_edit_test.main();
|
||||
document_with_database_test.main();
|
||||
document_with_inline_page_test.main();
|
||||
document_with_inline_math_equation_test.main();
|
||||
document_with_cover_image_test.main();
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/emoji.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('cover image', () {
|
||||
testWidgets('document cover tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
tester.expectToSeePluginAddCoverAndIconButton();
|
||||
|
||||
// Insert a document cover
|
||||
await tester.editor.tapOnAddCover();
|
||||
tester.expectToSeeDocumentCover(CoverType.asset);
|
||||
|
||||
// Hover over the cover to show the 'Change Cover' and delete buttons
|
||||
await tester.editor.hoverOnCover();
|
||||
tester.expectChangeCoverAndDeleteButton();
|
||||
|
||||
// Change cover to a solid color background
|
||||
await tester.editor.tapOnChangeCover();
|
||||
await tester.editor.switchSolidColorBackground();
|
||||
await tester.editor.dismissCoverPicker();
|
||||
tester.expectToSeeDocumentCover(CoverType.color);
|
||||
|
||||
// Change cover to a network image
|
||||
const imageUrl =
|
||||
"https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy/main/frontend/appflowy_flutter/assets/images/appflowy_launch_splash.jpg";
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnChangeCover();
|
||||
await tester.editor.addNetworkImageCover(imageUrl);
|
||||
await tester.editor.switchNetworkImageCover(imageUrl);
|
||||
await tester.editor.dismissCoverPicker();
|
||||
tester.expectToSeeDocumentCover(CoverType.file);
|
||||
|
||||
// Remove the cover
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnRemoveCover();
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
});
|
||||
|
||||
testWidgets('document icon tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
tester.expectToSeeDocumentIcon(null);
|
||||
|
||||
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
tester.expectToSeePluginAddCoverAndIconButton();
|
||||
|
||||
// Insert a document icon
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
tester.expectToSeeDocumentIcon('😀');
|
||||
|
||||
// Remove the document icon from the cover toolbar
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
await tester.editor.tapRemoveIconButton();
|
||||
tester.expectToSeeDocumentIcon(null);
|
||||
|
||||
// Add the icon back for further testing
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
tester.expectToSeeDocumentIcon('😀');
|
||||
|
||||
// Change the document icon
|
||||
await tester.editor.tapOnIconWidget();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😅');
|
||||
tester.expectToSeeDocumentIcon('😅');
|
||||
|
||||
// Remove the document icon from the icon picker
|
||||
await tester.editor.tapOnIconWidget();
|
||||
await tester.editor.tapRemoveIconButton(isInPicker: true);
|
||||
tester.expectToSeeDocumentIcon(null);
|
||||
});
|
||||
|
||||
testWidgets('icon and cover at the same time', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
tester.expectToSeeDocumentIcon(null);
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
tester.expectToSeePluginAddCoverAndIconButton();
|
||||
|
||||
// Insert a document icon
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// Insert a document cover
|
||||
await tester.editor.tapOnAddCover();
|
||||
|
||||
// Expect to see the icon and cover at the same time
|
||||
tester.expectToSeeDocumentIcon('😀');
|
||||
tester.expectToSeeDocumentCover(CoverType.asset);
|
||||
|
||||
// Hover over the cover toolbar and see that neither icons are shown
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
tester.expectToSeeEmptyDocumentHeaderToolbar();
|
||||
});
|
||||
});
|
||||
}
|
||||
+16
-26
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
@@ -7,23 +8,12 @@ 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();
|
||||
|
||||
group('database view in document', () {
|
||||
const location = 'database_view';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('insert a referenced grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -56,21 +46,21 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
// testWidgets('insert a referenced calendar', (tester) async {
|
||||
// await tester.initializeAppFlowy();
|
||||
// await tester.tapGoButton();
|
||||
testWidgets('insert a referenced calendar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
||||
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
||||
|
||||
// // validate the referenced grid is inserted
|
||||
// expect(
|
||||
// find.descendant(
|
||||
// of: find.byType(AppFlowyEditor),
|
||||
// matching: find.byType(CalendarPage),
|
||||
// ),
|
||||
// findsOneWidget,
|
||||
// );
|
||||
// });
|
||||
// validate the referenced grid is inserted
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(CalendarPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,7 +83,7 @@ Future<void> insertReferenceDatabase(
|
||||
);
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a referenced grid
|
||||
// insert a referenced view
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
layout.referencedMenuName,
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/ime.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('inline math equation in document', () {
|
||||
testWidgets('insert an inline math equation', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
const formula = 'E = MC ^ 2';
|
||||
await tester.ime.insertText(formula);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton = find.byTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
final inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsOneWidget);
|
||||
|
||||
// tap it and update the content
|
||||
await tester.tapButton(inlineMathEquation);
|
||||
final textFormField = find.descendant(
|
||||
of: find.byType(MathInputTextField),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
const newFormula = 'E = MC ^ 3';
|
||||
await tester.enterText(textFormField, newFormula);
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(MathInputTextField),
|
||||
matching: find.byType(FlowyButton),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('remove the inline math equation format', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
const formula = 'E = MC ^ 2';
|
||||
await tester.ime.insertText(formula);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
var inlineMathEquationButton = find.byTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
var inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsOneWidget);
|
||||
|
||||
// highlight the math equation block
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: 1),
|
||||
);
|
||||
|
||||
// expect to the see the inline math equation button is highlighted
|
||||
inlineMathEquationButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is IconItemWidget &&
|
||||
widget.tooltip ==
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
expect(
|
||||
tester.widget<IconItemWidget>(inlineMathEquationButton).isHighlight,
|
||||
isTrue,
|
||||
);
|
||||
|
||||
// cancel the format
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block is removed
|
||||
inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsNothing);
|
||||
|
||||
tester.expectToSeeText(formula);
|
||||
});
|
||||
});
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
+118
@@ -0,0 +1,118 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('inline page view in document', () {
|
||||
testWidgets('insert a inline page - grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await insertingInlinePage(tester, ViewLayoutPB.Grid);
|
||||
|
||||
final mentionBlock = find.byType(MentionPageBlock);
|
||||
expect(mentionBlock, findsOneWidget);
|
||||
await tester.tapButton(mentionBlock);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline page - board', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await insertingInlinePage(tester, ViewLayoutPB.Board);
|
||||
|
||||
final mentionBlock = find.byType(MentionPageBlock);
|
||||
expect(mentionBlock, findsOneWidget);
|
||||
await tester.tapButton(mentionBlock);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline page - calendar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await insertingInlinePage(tester, ViewLayoutPB.Calendar);
|
||||
|
||||
final mentionBlock = find.byType(MentionPageBlock);
|
||||
expect(mentionBlock, findsOneWidget);
|
||||
await tester.tapButton(mentionBlock);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline page - document', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await insertingInlinePage(tester, ViewLayoutPB.Document);
|
||||
|
||||
final mentionBlock = find.byType(MentionPageBlock);
|
||||
expect(mentionBlock, findsOneWidget);
|
||||
await tester.tapButton(mentionBlock);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline page and rename it', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document);
|
||||
|
||||
// rename
|
||||
await tester.hoverOnPageName(pageName);
|
||||
const newName = 'RenameToNewPageName';
|
||||
await tester.renamePage(newName);
|
||||
final finder = find.descendant(
|
||||
of: find.byType(MentionPageBlock),
|
||||
matching: find.findTextInFlowyText(newName),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline page and delete it', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid);
|
||||
|
||||
// rename
|
||||
await tester.hoverOnPageName(pageName);
|
||||
await tester.tapDeletePageButton();
|
||||
final finder = find.descendant(
|
||||
of: find.byType(MentionPageBlock),
|
||||
matching: find.findTextInFlowyText(pageName),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
await tester.tapButton(finder);
|
||||
expect(find.byType(FlowyErrorPage), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert a referenced database of [layout] into the document
|
||||
Future<String> insertingInlinePage(
|
||||
WidgetTester tester,
|
||||
ViewLayoutPB layout,
|
||||
) async {
|
||||
// create a new grid
|
||||
final id = uuid();
|
||||
final name = '${layout.name}_$id';
|
||||
await tester.createNewPageWithName(
|
||||
layout,
|
||||
name,
|
||||
);
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
'insert_a_inline_page_${layout.name}',
|
||||
);
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
await tester.editor.showAtMenu();
|
||||
await tester.editor.tapAtMenuItemWithName(name);
|
||||
return name;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/ime.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('edit document', () {
|
||||
testWidgets('redo & undo', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document called Sample
|
||||
const pageName = 'Sample';
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
|
||||
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert 1. to trigger it to be a numbered list
|
||||
await tester.ime.insertText('1. ');
|
||||
expect(find.text('1.', findRichText: true), findsOneWidget);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
|
||||
// undo
|
||||
// numbered list will be reverted to paragraph
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
ParagraphBlockKeys.type,
|
||||
);
|
||||
|
||||
// redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
|
||||
// switch to other page and switch back
|
||||
await tester.openPage(readme);
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// the numbered list should be kept
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('write a readme document', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document called Sample
|
||||
const pageName = 'Sample';
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
|
||||
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// mock inputting the sample
|
||||
final lines = _sample.split('\n');
|
||||
for (final line in lines) {
|
||||
await tester.ime.insertText(line);
|
||||
await tester.ime.insertCharacter('\n');
|
||||
}
|
||||
|
||||
// switch to other page and switch back
|
||||
await tester.openPage(readme);
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// this screenshots are different on different platform, so comment it out temporarily.
|
||||
// check the document
|
||||
// await expectLater(
|
||||
// find.byType(AppFlowyEditor),
|
||||
// matchesGoldenFile('document/edit_document_test.png'),
|
||||
// );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet.
|
||||
const _sample = r'''
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
---
|
||||
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
|
||||
|
||||
[] Type followed by bullet or num to create a list.
|
||||
|
||||
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
|
||||
|
||||
[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
|
||||
---
|
||||
* bulleted list 1
|
||||
|
||||
* bulleted list 2
|
||||
|
||||
* bulleted list 3
|
||||
bulleted list 4
|
||||
---
|
||||
1. numbered list 1
|
||||
|
||||
2. numbered list 2
|
||||
|
||||
3. numbered list 3
|
||||
numbered list 4
|
||||
---
|
||||
" quote''';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,95 @@
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/keyboard.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('hotkeys test', () {
|
||||
testWidgets('toggle theme mode', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectToSeeText(
|
||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
||||
);
|
||||
|
||||
await tester.tapButton(
|
||||
find.bySemanticsLabel(
|
||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tapButton(
|
||||
find.bySemanticsLabel(
|
||||
LocaleKeys.settings_appearance_themeMode_dark.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
await tester.tap(find.byType(SettingsDialog));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.keyL,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectToSeeText(
|
||||
LocaleKeys.settings_appearance_themeMode_light.tr(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('show or hide home menu', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HomeMenu), findsOneWidget);
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.backslash,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HomeMenu), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,23 +12,8 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('import files', () {
|
||||
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 multiple markdown files', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
@@ -38,18 +23,21 @@ void main() {
|
||||
await tester.tapImportButton();
|
||||
|
||||
final testFileNames = ['test1.md', 'test2.md'];
|
||||
final fileLocation = await tester.currentFileLocation();
|
||||
final paths = <String>[];
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/markdowns',
|
||||
fileName,
|
||||
),
|
||||
'assets/test/workspaces/markdowns/$fileName',
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
final path = p.join(context.applicationDataDirectory, fileName);
|
||||
paths.add(path);
|
||||
File(path).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: location);
|
||||
await mockPickFilePaths(
|
||||
paths: testFileNames
|
||||
.map((e) => p.join(context.applicationDataDirectory, e))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
await tester.tapTextAndMarkdownButton();
|
||||
|
||||
|
||||
@@ -8,16 +8,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUpAll(() async {
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'change the language successfully when launching the app for the first time',
|
||||
(tester) async {
|
||||
|
||||
@@ -17,7 +17,8 @@ void main() {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
|
||||
testWidgets('testing selection on open-ai smart menu replace', (tester) async {
|
||||
testWidgets('testing selection on open-ai smart menu replace',
|
||||
(tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
final editorState = appFlowyEditor.editorState;
|
||||
|
||||
@@ -38,7 +39,8 @@ void main() {
|
||||
await tester.tap(find.text('Summarize'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
|
||||
await tester
|
||||
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
@@ -49,7 +51,8 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
testWidgets('testing selection on open-ai smart menu insert', (tester) async {
|
||||
testWidgets('testing selection on open-ai smart menu insert',
|
||||
(tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
final editorState = appFlowyEditor.editorState;
|
||||
|
||||
@@ -69,7 +72,8 @@ void main() {
|
||||
await tester.tap(find.text('Summarize'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
|
||||
await tester
|
||||
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/ime.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('outline block test', () {
|
||||
testWidgets('insert an outline block', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
'outline_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await insertOutlineInDocument(tester);
|
||||
|
||||
// validate the outline is inserted
|
||||
expect(find.byType(OutlineBlockWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('insert an outline block and check if headings are visible',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
'outline_test',
|
||||
);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
await tester.ime.insertText('# Heading 1\n');
|
||||
await tester.ime.insertText('## Heading 2\n');
|
||||
await tester.ime.insertText('### Heading 3\n');
|
||||
|
||||
/* Results in:
|
||||
* # Heading 1
|
||||
* ## Heading 2
|
||||
* ### Heading 3
|
||||
*/
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(3);
|
||||
await insertOutlineInDocument(tester);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(OutlineBlockWidget),
|
||||
matching: find.text('Heading 1'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Heading 2 is prefixed with a bullet
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(OutlineBlockWidget),
|
||||
matching: find.text('Heading 2'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Heading 3 is prefixed with a dash
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(OutlineBlockWidget),
|
||||
matching: find.text('Heading 3'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// update the Heading 1 to Heading 1Hello world
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText('Hello world');
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(OutlineBlockWidget),
|
||||
matching: find.text('Heading 1Hello world'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Inserts an outline block in the document
|
||||
Future<void> insertOutlineInDocument(WidgetTester tester) async {
|
||||
// open the actions menu and insert the outline block
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_selectionMenu_outline.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
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 'share_markdown_test.dart' as share_markdown_test;
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_row_test.dart' as database_row_test;
|
||||
import 'database_setting_test.dart' as database_setting_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
import 'database_sort_test.dart' as database_sort_test;
|
||||
import 'database_view_test.dart' as database_view_test;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'import_files_test.dart' as import_files_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'share_markdown_test.dart' as share_markdown_test;
|
||||
import 'switch_folder_test.dart' as switch_folder_test;
|
||||
|
||||
/// The main task runner for all integration tests in AppFlowy.
|
||||
///
|
||||
@@ -17,11 +25,24 @@ 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_with_database_test.main();
|
||||
|
||||
// Document integration tests
|
||||
document_test_runner.startTesting();
|
||||
|
||||
// Database integration tests
|
||||
database_cell_test.main();
|
||||
database_field_test.main();
|
||||
database_share_test.main();
|
||||
database_row_page_test.main();
|
||||
database_row_test.main();
|
||||
database_setting_test.main();
|
||||
database_filter_test.main();
|
||||
database_sort_test.main();
|
||||
database_view_test.main();
|
||||
database_calendar_test.main();
|
||||
|
||||
// board_test.main();
|
||||
// empty_document_test.main();
|
||||
// smart_menu_test.main();
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -11,30 +11,17 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('share markdown in document page', () {
|
||||
const location = 'markdown';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('click the share button in document page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
// mock the file picker
|
||||
final path = await mockSaveFilePath(location, 'test.md');
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(context.applicationDataDirectory, 'test.md'),
|
||||
);
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
await tester.tapMarkdownButton();
|
||||
@@ -52,7 +39,7 @@ void main() {
|
||||
testWidgets(
|
||||
'share the markdown after renaming the document name',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
@@ -65,8 +52,12 @@ void main() {
|
||||
final shareButton = find.byType(ShareActionList);
|
||||
final shareButtonState =
|
||||
tester.state(shareButton) as ShareActionListState;
|
||||
final path =
|
||||
await mockSaveFilePath(location, '${shareButtonState.name}.md');
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(
|
||||
context.applicationDataDirectory,
|
||||
'${shareButtonState.name}.md',
|
||||
),
|
||||
);
|
||||
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -9,37 +11,26 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('customize the folder path', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
const String userA = 'userA';
|
||||
const String userB = 'userB';
|
||||
const userA = 'UserA';
|
||||
const userB = 'UserB';
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.cleanTestLocation(userB);
|
||||
await TestFolder.setTestLocation(userA);
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
final initialPath = p.join(userA, appFlowyDataFolder);
|
||||
final context = await tester.initializeAppFlowy(
|
||||
pathExtension: initialPath,
|
||||
);
|
||||
// remove the last extension
|
||||
final rootPath = context.applicationDataDirectory.replaceFirst(
|
||||
initialPath,
|
||||
'',
|
||||
);
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
// switch to user B
|
||||
{
|
||||
// set user name to userA
|
||||
// set user name for userA
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userA);
|
||||
@@ -48,12 +39,14 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userB);
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userB),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
// set user name to userB
|
||||
// set user name for userB
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userB);
|
||||
@@ -65,7 +58,9 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userA);
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userA),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
@@ -80,16 +75,15 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userB);
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userB),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeHomePage();
|
||||
tester.expectToSeeUserName(userB);
|
||||
}
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.cleanTestLocation(userB);
|
||||
});
|
||||
|
||||
testWidgets('reset to default location', (tester) async {
|
||||
@@ -106,8 +100,8 @@ void main() {
|
||||
await tester.restoreLocation();
|
||||
|
||||
expect(
|
||||
await TestFolder.defaultDevelopmentLocation(),
|
||||
await TestFolder.currentLocation(),
|
||||
await appFlowyApplicationDataDirectory().then((value) => value.path),
|
||||
await getIt<ApplicationDataStorage>().getPath(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +1,95 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/main.dart' as app;
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class TestFolder {
|
||||
/// Location / Path
|
||||
class FlowyTestContext {
|
||||
FlowyTestContext({
|
||||
required this.applicationDataDirectory,
|
||||
});
|
||||
|
||||
/// Set a given AppFlowy data storage location under test environment.
|
||||
///
|
||||
/// To pass null means clear the location.
|
||||
///
|
||||
/// The file_picker is a system component and can't be tapped, so using logic instead of tapping.
|
||||
///
|
||||
static Future<void> setTestLocation(String? name) async {
|
||||
final location = await testLocation(name);
|
||||
SharedPreferences.setMockInitialValues({
|
||||
KVKeys.pathLocation: location.path,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/// Clean the location.
|
||||
static Future<void> cleanTestLocation(String? name) async {
|
||||
final dir = await testLocation(name);
|
||||
await dir.delete(recursive: true);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Get current using location.
|
||||
static Future<String> currentLocation() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(KVKeys.pathLocation)!;
|
||||
}
|
||||
|
||||
/// Get default location under development environment.
|
||||
static Future<String> defaultDevelopmentLocation() async {
|
||||
final dir = await appFlowyDocumentDirectory();
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
/// Get default location under test environment.
|
||||
static Future<Directory> testLocation(String? name) async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
var path = '${dir.path}/flowy_test';
|
||||
if (name != null) {
|
||||
path += '/$name';
|
||||
}
|
||||
return Directory(path).create(recursive: true);
|
||||
}
|
||||
final String applicationDataDirectory;
|
||||
}
|
||||
|
||||
extension AppFlowyTestBase on WidgetTester {
|
||||
Future<void> initializeAppFlowy() async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('hotkey_manager'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
Future<FlowyTestContext> initializeAppFlowy({
|
||||
// use to append after the application data directory
|
||||
String? pathExtension,
|
||||
Size windowsSize = const Size(1600, 1200),
|
||||
}) async {
|
||||
binding.setSurfaceSize(windowsSize);
|
||||
|
||||
return;
|
||||
},
|
||||
mockHotKeyManagerHandlers();
|
||||
final directory = await mockApplicationDataStorage(
|
||||
pathExtension: pathExtension,
|
||||
);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await FlowyRunner.run(
|
||||
FlowyApp(),
|
||||
IntegrationMode.integrationTest,
|
||||
);
|
||||
|
||||
await app.main();
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
return FlowyTestContext(
|
||||
applicationDataDirectory: directory,
|
||||
);
|
||||
}
|
||||
|
||||
void mockHotKeyManagerHandlers() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> mockApplicationDataStorage({
|
||||
// use to append after the application data directory
|
||||
String? pathExtension,
|
||||
}) async {
|
||||
final dir = await getTemporaryDirectory();
|
||||
|
||||
// Use a random uuid to avoid conflict.
|
||||
String path = p.join(dir.path, 'appflowy_integration_test', uuid());
|
||||
if (pathExtension != null && pathExtension.isNotEmpty) {
|
||||
path = '$path/$pathExtension';
|
||||
}
|
||||
final directory = Directory(path);
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
|
||||
MockApplicationDataStorage.initialPath = directory.path;
|
||||
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<void> tapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = true,
|
||||
bool warnIfMissed = false,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(finder);
|
||||
await tap(
|
||||
finder,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
}
|
||||
@@ -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(
|
||||
@@ -121,6 +127,43 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> doubleTapAt(
|
||||
Offset location, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tapAt(location, pointer: pointer, buttons: buttons);
|
||||
await pump(kDoubleTapMinTime);
|
||||
await tapAt(location, pointer: pointer, buttons: buttons);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> doubleTapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(
|
||||
finder,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
|
||||
await pump(kDoubleTapMinTime);
|
||||
|
||||
await tap(
|
||||
finder,
|
||||
buttons: buttons,
|
||||
pointer: pointer,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> wait(int milliseconds) async {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
@@ -130,7 +173,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,16 +11,12 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
extension CommonOperations on WidgetTester {
|
||||
/// Get current file location of AppFlowy.
|
||||
Future<String> currentFileLocation() async {
|
||||
return TestFolder.currentLocation();
|
||||
}
|
||||
|
||||
/// Tap the GetStart button on the launch page.
|
||||
Future<void> tapGoButton() async {
|
||||
final goButton = find.byType(GoButton);
|
||||
@@ -47,6 +43,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 +143,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 +240,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 {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,15 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'ime.dart';
|
||||
@@ -13,21 +24,117 @@ 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);
|
||||
final textBlocks = find.byType(AppFlowyRichText);
|
||||
index = index.clamp(0, textBlocks.evaluate().length - 1);
|
||||
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Hover on cover plugin button above the document
|
||||
Future<void> hoverOnCoverPluginAddButton() async {
|
||||
final editor = find.byWidgetPredicate(
|
||||
(widget) => widget is AppFlowyEditor,
|
||||
Future<void> hoverOnCoverToolbar() async {
|
||||
final coverToolbar = find.byType(DocumentHeaderToolbar);
|
||||
await tester.startGesture(
|
||||
tester.getBottomLeft(coverToolbar).translate(5, -5),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
editor,
|
||||
offset: tester.getTopLeft(editor).translate(20, 20),
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Taps on the 'Add Icon' button in the cover toolbar
|
||||
Future<void> tapAddIconButton() async {
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||
);
|
||||
expect(find.byType(EmojiPopover), findsOneWidget);
|
||||
}
|
||||
|
||||
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
|
||||
Future<void> tapRemoveIconButton({bool isInPicker = false}) async {
|
||||
Finder button =
|
||||
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
|
||||
if (isInPicker) {
|
||||
button = find.descendant(of: find.byType(EmojiPopover), matching: button);
|
||||
}
|
||||
|
||||
await tester.tapButton(button);
|
||||
}
|
||||
|
||||
/// Requires that the document must already have an icon. This opens the icon
|
||||
/// picker
|
||||
Future<void> tapOnIconWidget() async {
|
||||
final iconWidget = find.byType(EmojiIconWidget);
|
||||
await tester.tapButton(iconWidget);
|
||||
}
|
||||
|
||||
Future<void> tapOnAddCover() async {
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tapOnChangeCover() async {
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> switchSolidColorBackground() async {
|
||||
final findPurpleButton = find.byWidgetPredicate(
|
||||
(widget) => widget is ColorItem && widget.option.colorHex == "ffe8e0ff",
|
||||
);
|
||||
await tester.tapButton(findPurpleButton);
|
||||
}
|
||||
|
||||
Future<void> addNetworkImageCover(String imageUrl) async {
|
||||
final findNewImageButton = find.byType(NewCustomCoverButton);
|
||||
await tester.tapButton(findNewImageButton);
|
||||
|
||||
final imageUrlTextField = find.descendant(
|
||||
of: find.byType(NetworkImageUrlInput),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
await tester.enterText(imageUrlTextField, imageUrl);
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_cover_add.tr(),
|
||||
);
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> switchNetworkImageCover(String imageUrl) async {
|
||||
final image = find.byWidgetPredicate(
|
||||
(widget) => widget is ImageGridItem,
|
||||
);
|
||||
await tester.tapButton(image);
|
||||
}
|
||||
|
||||
Future<void> tapOnRemoveCover() async {
|
||||
await tester.tapButton(find.byType(DeleteCoverButton));
|
||||
}
|
||||
|
||||
/// A cover must be present in the document to function properly since this
|
||||
/// catches all cover types collectively
|
||||
Future<void> hoverOnCover() async {
|
||||
final cover = find.byType(DocumentCover);
|
||||
await tester.startGesture(
|
||||
tester.getCenter(cover),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> dismissCoverPicker() async {
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// trigger the slash command (selection menu)
|
||||
@@ -35,6 +142,11 @@ class EditorOperations {
|
||||
await tester.ime.insertCharacter('/');
|
||||
}
|
||||
|
||||
/// trigger the slash command (selection menu)
|
||||
Future<void> showAtMenu() async {
|
||||
await tester.ime.insertCharacter('@');
|
||||
}
|
||||
|
||||
/// Tap the slash menu item with [name]
|
||||
///
|
||||
/// Must call [showSlashMenu] first.
|
||||
@@ -42,4 +154,25 @@ class EditorOperations {
|
||||
final slashMenuItem = find.text(name, findRichText: true);
|
||||
await tester.tapButton(slashMenuItem);
|
||||
}
|
||||
|
||||
/// Tap the at menu item with [name]
|
||||
///
|
||||
/// Must call [showAtMenu] first.
|
||||
Future<void> tapAtMenuItemWithName(String name) async {
|
||||
final atMenuItem = find.descendant(
|
||||
of: find.byType(SelectionMenuWidget),
|
||||
matching: find.text(name, findRichText: true),
|
||||
);
|
||||
await tester.tapButton(atMenuItem);
|
||||
}
|
||||
|
||||
/// Update the editor's selection
|
||||
Future<void> updateSelection(Selection selection) async {
|
||||
final editorState = getCurrentEditorState();
|
||||
editorState.updateSelectionWithReason(
|
||||
selection,
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
extension EmojiTestExtension on WidgetTester {
|
||||
/// Must call [openEmojiPicker] first
|
||||
Future<void> switchToEmojiList() async {
|
||||
final icon = find.byIcon(Icons.tag_faces);
|
||||
await tapButton(icon);
|
||||
}
|
||||
|
||||
Future<void> tapEmoji(String emoji) async {
|
||||
final emojiWidget = find.text(emoji);
|
||||
await tapButton(emojiWidget);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -12,7 +14,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,12 +44,12 @@ 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);
|
||||
}
|
||||
|
||||
/// Expect to see the add button and icon button inside the document.
|
||||
/// Expect to see the add button and icon button in the cover toolbar
|
||||
void expectToSeePluginAddCoverAndIconButton() {
|
||||
final addCover = find.textContaining(
|
||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||
@@ -59,10 +61,55 @@ extension Expectation on WidgetTester {
|
||||
expect(addIcon, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see the document header toolbar empty
|
||||
void expectToSeeEmptyDocumentHeaderToolbar() {
|
||||
final addCover = find.textContaining(
|
||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||
);
|
||||
final addIcon = find.textContaining(
|
||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||
);
|
||||
expect(addCover, findsNothing);
|
||||
expect(addIcon, findsNothing);
|
||||
}
|
||||
|
||||
void expectToSeeDocumentIcon(String? emoji) {
|
||||
if (emoji == null) {
|
||||
final iconWidget = find.byType(EmojiIconWidget);
|
||||
expect(iconWidget, findsNothing);
|
||||
return;
|
||||
}
|
||||
final iconWidget = find.byWidgetPredicate(
|
||||
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
|
||||
);
|
||||
expect(iconWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
void expectToSeeDocumentCover(CoverType type) {
|
||||
final findCover = find.byWidgetPredicate(
|
||||
(widget) => widget is DocumentCover && widget.coverType == type,
|
||||
);
|
||||
expect(findCover, findsOneWidget);
|
||||
}
|
||||
|
||||
void expectToSeeNoDocumentCover() {
|
||||
final findCover = find.byType(DocumentCover);
|
||||
expect(findCover, findsNothing);
|
||||
}
|
||||
|
||||
void expectChangeCoverAndDeleteButton() {
|
||||
final findChangeCover = find.text(
|
||||
LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
);
|
||||
final findRemoveIcon = find.byType(DeleteCoverButton);
|
||||
expect(findChangeCover, findsOneWidget);
|
||||
expect(findRemoveIcon, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see the user name on the home page
|
||||
void expectToSeeUserName(String name) {
|
||||
final userName = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.title == name,
|
||||
(widget) => widget is FlowyText && widget.text == name,
|
||||
);
|
||||
expect(userName, findsOneWidget);
|
||||
}
|
||||
@@ -72,7 +119,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 +129,7 @@ extension Expectation on WidgetTester {
|
||||
Finder findPageName(String name) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is ViewSectionItem && widget.view.name == name,
|
||||
skipOffstage: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@ extension IME on WidgetTester {
|
||||
|
||||
class IMESimulator {
|
||||
IMESimulator(this.tester) {
|
||||
client = findDeltaTextInputClient();
|
||||
client = findTextInputClient();
|
||||
}
|
||||
|
||||
final WidgetTester tester;
|
||||
late final DeltaTextInputClient client;
|
||||
late final TextInputClient client;
|
||||
|
||||
Future<void> insertText(String text) async {
|
||||
for (final c in text.characters) {
|
||||
@@ -27,28 +27,22 @@ class IMESimulator {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
final deltas = [
|
||||
TextEditingDeltaInsertion(
|
||||
textInserted: character,
|
||||
oldText: value.text.replaceRange(
|
||||
value.selection.start,
|
||||
value.selection.end,
|
||||
'',
|
||||
),
|
||||
insertionOffset: value.selection.baseOffset,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.baseOffset + 1,
|
||||
),
|
||||
composing: TextRange.empty,
|
||||
final text = value.text
|
||||
.replaceRange(value.selection.start, value.selection.end, character);
|
||||
final textEditingValue = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.baseOffset + 1,
|
||||
),
|
||||
];
|
||||
client.updateEditingValueWithDeltas(deltas);
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
client.updateEditingValue(textEditingValue);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
DeltaTextInputClient findDeltaTextInputClient() {
|
||||
TextInputClient findTextInputClient() {
|
||||
final finder = find.byType(KeyboardServiceWidget);
|
||||
final KeyboardServiceWidgetState state = tester.state(finder);
|
||||
return state.textInputService as DeltaTextInputClient;
|
||||
return state.textInputService as TextInputClient;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:file_picker/file_picker.dart' as fp;
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../util.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
|
||||
class MockFilePicker implements FilePickerService {
|
||||
MockFilePicker({
|
||||
@@ -25,7 +20,7 @@ class MockFilePicker implements FilePickerService {
|
||||
String? dialogTitle,
|
||||
String? fileName,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
@@ -36,18 +31,17 @@ class MockFilePicker implements FilePickerService {
|
||||
Future<FilePickerResult?> pickFiles({
|
||||
String? dialogTitle,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
Function(fp.FilePickerStatus p1)? onFileLoading,
|
||||
Function(FilePickerStatus p1)? onFileLoading,
|
||||
bool allowCompression = true,
|
||||
bool allowMultiple = false,
|
||||
bool withData = false,
|
||||
bool withReadStream = false,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
final platformFiles = mockPaths
|
||||
.map((e) => fp.PlatformFile(path: e, name: '', size: 0))
|
||||
.toList();
|
||||
final platformFiles =
|
||||
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
|
||||
return Future.value(
|
||||
FilePickerResult(
|
||||
platformFiles,
|
||||
@@ -56,20 +50,21 @@ class MockFilePicker implements FilePickerService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mockGetDirectoryPath(String? name) async {
|
||||
final dir = await TestFolder.testLocation(name);
|
||||
Future<void> mockGetDirectoryPath(
|
||||
String path,
|
||||
) async {
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPath: dir.path,
|
||||
mockPath: path,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<String> mockSaveFilePath(String? name, String fileName) async {
|
||||
final dir = await TestFolder.testLocation(name);
|
||||
final path = p.join(dir.path, fileName);
|
||||
Future<String> mockSaveFilePath(
|
||||
String path,
|
||||
) async {
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
@@ -79,18 +74,16 @@ Future<String> mockSaveFilePath(String? name, String fileName) async {
|
||||
return path;
|
||||
}
|
||||
|
||||
Future<List<String>> mockPickFilePaths(
|
||||
List<String> fileNames, {
|
||||
String? name,
|
||||
String? customPath,
|
||||
Future<List<String>> mockPickFilePaths({
|
||||
required List<String> paths,
|
||||
}) async {
|
||||
late final Directory dir;
|
||||
if (customPath != null) {
|
||||
dir = Directory(customPath);
|
||||
} else {
|
||||
dir = await TestFolder.testLocation(name);
|
||||
}
|
||||
final paths = fileNames.map((e) => p.join(dir.path, e)).toList();
|
||||
// late final Directory dir;
|
||||
// if (customPath != null) {
|
||||
// dir = Directory(customPath);
|
||||
// } else {
|
||||
// dir = await TestFolder.testLocation(applicationDataPath, name);
|
||||
// }
|
||||
// final paths = fileNames.map((e) => p.join(dir.path, e)).toList();
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
|
||||
@@ -7,32 +7,24 @@ class Config {
|
||||
required String anonKey,
|
||||
required String key,
|
||||
required String secret,
|
||||
required String pgUrl,
|
||||
required String pgUser,
|
||||
required String pgPassword,
|
||||
required String pgPort,
|
||||
}) async {
|
||||
final postgresConfig = PostgresConfigurationPB.create()
|
||||
..url = pgUrl
|
||||
..userName = pgUser
|
||||
..password = pgPassword
|
||||
..port = int.parse(pgPort);
|
||||
|
||||
await ConfigEventSetSupabaseConfig(
|
||||
SupabaseConfigPB.create()
|
||||
..supabaseUrl = url
|
||||
..key = key
|
||||
..anonKey = anonKey
|
||||
..jwtSecret = secret,
|
||||
..jwtSecret = secret
|
||||
..postgresConfig = postgresConfig,
|
||||
).send();
|
||||
}
|
||||
|
||||
static Future<void> setSupabaseCollabPluginConfig({
|
||||
required String url,
|
||||
required String key,
|
||||
required String jwtSecret,
|
||||
required String collabTable,
|
||||
}) async {
|
||||
final payload = CollabPluginConfigPB.create();
|
||||
final collabTableConfig = CollabTableConfigPB.create()
|
||||
..tableName = collabTable;
|
||||
|
||||
payload.supabaseConfig = SupabaseDBConfigPB.create()
|
||||
..supabaseUrl = url
|
||||
..key = key
|
||||
..jwtSecret = jwtSecret
|
||||
..collabTableConfig = collabTableConfig;
|
||||
|
||||
await ConfigEventSetCollabPluginConfig(payload).send();
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,9 @@ class KVKeys {
|
||||
/// The value is a json string with the following format:
|
||||
/// {'height': 600.0, 'width': 800.0}
|
||||
static const String windowSize = 'windowSize';
|
||||
|
||||
static const String kDocumentAppearanceFontSize =
|
||||
'kDocumentAppearanceFontSize';
|
||||
static const String kDocumentAppearanceFontFamily =
|
||||
'kDocumentAppearanceFontFamily';
|
||||
}
|
||||
+39
-3
@@ -3,6 +3,17 @@ import 'package:envied/envied.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
|
||||
/// The environment variables are defined in `.env` file that is located in the
|
||||
/// appflowy_flutter.
|
||||
/// Run `dart run build_runner build --delete-conflicting-outputs`
|
||||
/// to generate the keys from the env file.
|
||||
///
|
||||
/// If you want to regenerate the keys, you need to run `dart run
|
||||
/// build_runner clean` before running `dart run build_runner build
|
||||
/// --delete-conflicting-outputs`.
|
||||
|
||||
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
|
||||
///
|
||||
@Envied(path: '.env')
|
||||
abstract class Env {
|
||||
@EnviedField(
|
||||
@@ -32,14 +43,39 @@ abstract class Env {
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_COLLAB_TABLE',
|
||||
varName: 'SUPABASE_DB',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseCollabTable = _Env.supabaseCollabTable;
|
||||
static final String supabaseDb = _Env.supabaseDb;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_DB_USER',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseDbUser = _Env.supabaseDbUser;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_DB_PASSWORD',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseDbPassword = _Env.supabaseDbPassword;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_DB_PORT',
|
||||
defaultValue: '5432',
|
||||
)
|
||||
static final String supabaseDbPort = _Env.supabaseDbPort;
|
||||
}
|
||||
|
||||
bool get isSupabaseEnable =>
|
||||
Env.supabaseUrl.isNotEmpty &&
|
||||
Env.supabaseAnonKey.isNotEmpty &&
|
||||
Env.supabaseKey.isNotEmpty &&
|
||||
Env.supabaseJwtSecret.isNotEmpty;
|
||||
Env.supabaseJwtSecret.isNotEmpty &&
|
||||
Env.supabaseDb.isNotEmpty &&
|
||||
Env.supabaseDbUser.isNotEmpty &&
|
||||
Env.supabaseDbPassword.isNotEmpty &&
|
||||
Env.supabaseDbPort.isNotEmpty;
|
||||
@@ -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];
|
||||
}
|
||||
+9
-7
@@ -113,7 +113,7 @@ class CalendarDayCard extends StatelessWidget {
|
||||
.add(CalendarEvent.moveEvent(event, date));
|
||||
},
|
||||
),
|
||||
_NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
MouseRegion(
|
||||
onEnter: (p) => notifyEnter(context, true),
|
||||
onExit: (p) => notifyEnter(context, false),
|
||||
@@ -160,9 +160,10 @@ class _Header extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NewEventButton extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class NewEventButton extends StatelessWidget {
|
||||
final VoidCallback onCreate;
|
||||
const _NewEventButton({required this.onCreate, Key? key}) : super(key: key);
|
||||
const NewEventButton({required this.onCreate, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -254,7 +255,7 @@ class _EventList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Flexible(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) => _EventCard(
|
||||
itemBuilder: (BuildContext context, int index) => EventCard(
|
||||
event: events[index],
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
@@ -270,13 +271,14 @@ class _EventList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _EventCard extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class EventCard extends StatelessWidget {
|
||||
final CalendarDayEvent event;
|
||||
final String viewId;
|
||||
final RowCache rowCache;
|
||||
final BoxConstraints constraints;
|
||||
|
||||
const _EventCard({
|
||||
const EventCard({
|
||||
required this.event,
|
||||
required this.viewId,
|
||||
required this.rowCache,
|
||||
@@ -309,7 +311,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];
|
||||
}
|
||||
+195
-133
@@ -1,5 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
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 +19,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 +102,11 @@ class GridPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridPageState extends State<GridPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@@ -58,21 +117,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 +125,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 +139,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,115 +174,148 @@ 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: _renderList(context, state, rowInfos),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderList(
|
||||
BuildContext context,
|
||||
GridState state,
|
||||
List<RowInfo> rowInfos,
|
||||
) {
|
||||
if (Platform.isWindows) {
|
||||
// Workaround: On Windows, the focusing of the text cell is not working
|
||||
// properly when the list is reorderable. So using the ListView instead.
|
||||
return ListView.builder(
|
||||
controller: scrollController.verticalController,
|
||||
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: false,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return 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'));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderRow(
|
||||
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 +337,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 +392,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 +422,8 @@ class _WrapScrollView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RowCountBadge extends StatelessWidget {
|
||||
const _RowCountBadge();
|
||||
class _GridFooter extends StatelessWidget {
|
||||
const _GridFooter();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -369,7 +431,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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 685 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user