Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd629aaef | ||
|
|
d302f6b3fb | ||
|
|
5595649f53 | ||
|
|
7de17ab0b8 | ||
|
|
13ff38756f | ||
|
|
197e5b3a79 | ||
|
|
5cced92646 | ||
|
|
e50d708c21 | ||
|
|
79fc7c4cfe | ||
|
|
202cd90711 | ||
|
|
177f7c4fa3 | ||
|
|
d986e01d3e | ||
|
|
8a8915c662 | ||
|
|
efc857d752 | ||
|
|
14dee6b797 | ||
|
|
63eee8b415 | ||
|
|
d96a1d8bd4 | ||
|
|
430325c731 | ||
|
|
d5884ad2b5 | ||
|
|
95f8b2e9a4 | ||
|
|
27dd719aa8 | ||
|
|
b8983e4466 | ||
|
|
00c0934df6 | ||
|
|
aca3c90eb3 | ||
|
|
8b4d1868e5 | ||
|
|
5bcf48a4f9 | ||
|
|
335861706e | ||
|
|
4f5672c2af | ||
|
|
b5d9a73194 | ||
|
|
619d82f115 | ||
|
|
d5887b0a12 | ||
|
|
c7f73551c7 | ||
|
|
ed04e247ba | ||
|
|
2ecd0a67ad | ||
|
|
4ce94a9ceb | ||
|
|
d4e39389d2 | ||
|
|
ab5a3dae3c | ||
|
|
133ac72317 | ||
|
|
e9be37a961 | ||
|
|
0f9f5251f2 |
No files matched your search
@@ -23,7 +23,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.65"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
with:
|
||||
prefix-key: ${{ matrix.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/rust-lib
|
||||
|
||||
- uses: davidB/rust-cargo-make@v1
|
||||
with:
|
||||
@@ -102,6 +102,7 @@ jobs:
|
||||
git config --system core.longpaths true
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
dart pub global activate protoc_plugin 20.0.1
|
||||
shell: bash
|
||||
|
||||
- name: Build AppFlowy
|
||||
@@ -118,3 +119,17 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make dart_unit_test
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
with:
|
||||
action: codecov/codecov-action@v3
|
||||
with: |
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_unit_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
attempt_limit: 5
|
||||
attempt_delay: 10000
|
||||
|
||||
@@ -6,17 +6,23 @@ on:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/appflowy_flutter/**"
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/appflowy_flutter/**"
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -32,31 +38,36 @@ jobs:
|
||||
with:
|
||||
toolchain: "stable-2022-04-07"
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.10.1"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v2
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
prefix-key: ${{ matrix.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
|
||||
- name: Cache Rust
|
||||
id: cache-rust-target
|
||||
uses: actions/cache@v2
|
||||
- uses: davidB/rust-cargo-make@v1
|
||||
with:
|
||||
path: |
|
||||
frontend/rust-lib/target
|
||||
shared-lib/target
|
||||
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
version: '0.36.6'
|
||||
|
||||
- name: Setup Environment
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
@@ -69,14 +80,8 @@ jobs:
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- if: steps.cache-cargo.outputs.cache-hit != 'true'
|
||||
name: Rust Deps
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
|
||||
- name: Config Flutter
|
||||
run: |
|
||||
@@ -87,6 +92,7 @@ jobs:
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
dart pub global activate protoc_plugin 20.0.1
|
||||
shell: bash
|
||||
|
||||
- name: Build Test lib
|
||||
@@ -116,10 +122,15 @@ jobs:
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
# - uses: codecov/codecov-action@v3
|
||||
# with:
|
||||
# name: appflowy
|
||||
# flags: appflowy
|
||||
# env_vars: ${{ matrix.os }}
|
||||
# fail_ci_if_error: true
|
||||
# verbose: true
|
||||
- name: Upload coverage to Codecov
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
with:
|
||||
action: codecov/codecov-action@v3
|
||||
with: |
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_integrateion_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
attempt_limit: 5
|
||||
attempt_delay: 10000
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.65"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
|
||||
@@ -21,7 +21,7 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_TOOLCHAIN: "1.65"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.65"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.65"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
tauri-build:
|
||||
|
||||
@@ -1,4 +1,65 @@
|
||||
# Release Notes
|
||||
## Version 0.2.3 - 06/21/2023
|
||||
### New Features
|
||||
- Added support for creating multiple database views for existing database
|
||||
|
||||
## Version 0.2.2 - 06/15/2023
|
||||
### New Features
|
||||
- Added support for embedding a document in the database's row detail page
|
||||
- Added support for inserting an emoji in the database's row detail page
|
||||
|
||||
### Other Updates
|
||||
- Added language selector on the welcome page
|
||||
- Added support for importing multiple markdown files all at once
|
||||
|
||||
## Version 0.2.1 - 06/11/2023
|
||||
### New Features
|
||||
- Added support for creating or referencing a calendar in the document
|
||||
- Added `+` icon in grid's add field
|
||||
|
||||
### Other Updates
|
||||
- Added vertical padding for progress bar
|
||||
- Hide url cell accessory when the content is empty
|
||||
|
||||
### Bug fixes:
|
||||
- Fixed unable to export markdown
|
||||
- Fixed adding vertical padding for progress bar
|
||||
- Fixed database view didn't update after the database layout changed.
|
||||
|
||||
## Version 0.2.0 - 06/08/2023
|
||||
|
||||
### New Features
|
||||
- Improved checklists to support each cell having its own list
|
||||
- Drag and drop calendar events
|
||||
- Switch layouts (calendar, grid, kanban) of a database
|
||||
- New database properties: 'Updated At' and 'Created At'
|
||||
- Enabled hiding properties on the row detail page
|
||||
- Added support for reordering and saving row order in different database views.
|
||||
- Enabled each database view to have its own settings, including filter and sort options
|
||||
- Added support to convert `“` (double quote) into a block quote
|
||||
- Added support to convert `***` (three stars) into a divider
|
||||
- Added support for an 'Add' button to insert a paragraph in a document and display the slash menu
|
||||
- Added support for an 'Option' button to delete, duplicate, and customize block actions
|
||||
|
||||
### Other Updates
|
||||
- Added support for importing v0.1.x documents and databases
|
||||
- Added support for database import and export to CSV
|
||||
- Optimized scroll behavior in documents.
|
||||
- Redesigned the launch page
|
||||
|
||||
### Bug fixes
|
||||
- Fixed bugs related to numbers
|
||||
- Fixed issues with referenced databases in documents
|
||||
- Fixed menu overflow issues in documents
|
||||
|
||||
### Data migration
|
||||
The data format of this version is not compatible with previous versions. Therefore, to migrate your data to the new version, you need to use the export and import functions. Please follow the guide to learn how to export and import your data.
|
||||
|
||||
#### Export files in v0.1.6
|
||||
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9
|
||||
|
||||
#### Import files in v0.2.0
|
||||
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2
|
||||
|
||||
## Version 0.1.5 - 11/05/2023
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.2.0"
|
||||
CURRENT_APP_VERSION = "0.2.3"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -70,3 +70,7 @@ windows/flutter/dart_ffi/
|
||||
**/.vscode/
|
||||
|
||||
*.env
|
||||
|
||||
coverage/
|
||||
|
||||
**/failures/*.png
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Calendar / Calendar_Days">
|
||||
<path id="Vector" d="M8 4H7.2002C6.08009 4 5.51962 4 5.0918 4.21799C4.71547 4.40973 4.40973 4.71547 4.21799 5.0918C4 5.51962 4 6.08009 4 7.2002V8M8 4H16M8 4V2M16 4H16.8002C17.9203 4 18.4796 4 18.9074 4.21799C19.2837 4.40973 19.5905 4.71547 19.7822 5.0918C20 5.5192 20 6.07899 20 7.19691V8M16 4V2M4 8V16.8002C4 17.9203 4 18.4801 4.21799 18.9079C4.40973 19.2842 4.71547 19.5905 5.0918 19.7822C5.5192 20 6.07899 20 7.19691 20H16.8031C17.921 20 18.48 20 18.9074 19.7822C19.2837 19.5905 19.5905 19.2842 19.7822 18.9079C20 18.4805 20 17.9215 20 16.8036V8M4 8H20M16 16H16.002L16.002 16.002L16 16.002V16ZM12 16H12.002L12.002 16.002L12 16.002V16ZM8 16H8.002L8.00195 16.002L8 16.002V16ZM16.002 12V12.002L16 12.002V12H16.002ZM12 12H12.002L12.002 12.002L12 12.002V12ZM8 12H8.002L8.00195 12.002L8 12.002V12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="-0.02 0 122.88 122.88" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="enable-background:new 0 0 122.84 122.88" xml:space="preserve">
|
||||
|
||||
<g>
|
||||
|
||||
<path d="M81.54,4.71c0-2.62,2.58-4.71,5.77-4.71c3.2,0,5.77,2.13,5.77,4.71V25.4c0,2.62-2.58,4.71-5.77,4.71 c-3.2,0-5.77-2.13-5.77-4.71V4.71L81.54,4.71z M20.4,89.87l7.55-0.47c0.16,1.22,0.5,2.16,1,2.79c0.82,1.04,1.99,1.56,3.51,1.56 c1.13,0,2.01-0.26,2.62-0.8C35.69,92.41,36,91.8,36,91.1c0-0.66-0.29-1.26-0.87-1.79c-0.58-0.53-1.93-1.02-4.06-1.49 c-3.49-0.78-5.96-1.82-7.45-3.12c-1.5-1.29-2.25-2.95-2.25-4.96c0-1.32,0.38-2.56,1.15-3.74c0.77-1.18,1.92-2.1,3.46-2.77 c1.54-0.67,3.65-1.01,6.32-1.01c3.29,0,5.79,0.61,7.52,1.84c1.72,1.22,2.75,3.17,3.08,5.84l-7.47,0.44 c-0.2-1.17-0.62-2.02-1.25-2.55c-0.64-0.53-1.52-0.8-2.64-0.8c-0.92,0-1.62,0.2-2.09,0.59c-0.47,0.39-0.7,0.87-0.7,1.43 c0,0.41,0.19,0.77,0.57,1.1c0.37,0.34,1.25,0.65,2.65,0.95c3.47,0.75,5.96,1.51,7.46,2.28c1.5,0.77,2.6,1.71,3.28,2.85 c0.68,1.13,1.02,2.4,1.02,3.81c0,1.65-0.46,3.17-1.37,4.56c-0.92,1.39-2.19,2.45-3.83,3.17c-1.64,0.72-3.7,1.08-6.19,1.08 c-4.37,0-7.4-0.84-9.09-2.53C21.58,94.6,20.62,92.46,20.4,89.87L20.4,89.87z M64.53,72.65h7.94v15.33c0,1.52-0.24,2.95-0.71,4.3 c-0.47,1.35-1.22,2.53-2.23,3.55c-1.01,1.01-2.07,1.72-3.18,2.13c-1.54,0.57-3.4,0.86-5.56,0.86c-1.25,0-2.62-0.09-4.1-0.26 c-1.48-0.17-2.72-0.52-3.71-1.04c-0.99-0.52-1.9-1.26-2.72-2.22c-0.83-0.96-1.39-1.95-1.69-2.96c-0.49-1.64-0.74-3.08-0.74-4.35 V72.65h7.94v15.69c0,1.4,0.39,2.5,1.16,3.28c0.78,0.79,1.86,1.19,3.23,1.19c1.36,0,2.43-0.39,3.21-1.17 c0.77-0.77,1.16-1.87,1.16-3.3V72.65L64.53,72.65z M77.85,72.65h7.42l9.65,14.21V72.65h7.51v25.73h-7.51l-9.59-14.13v14.13h-7.47 V72.65L77.85,72.65z M29.53,4.71c0-2.62,2.58-4.71,5.77-4.71c3.2,0,5.77,2.13,5.77,4.71V25.4c0,2.62-2.58,4.71-5.77,4.71 c-3.2,0-5.77-2.13-5.77-4.71V4.71L29.53,4.71z M7.56,44.09h107.62V22.66c0-0.8-0.31-1.55-0.84-2.04c-0.53-0.53-1.24-0.84-2.04-0.84 h-9.31c-1.78,0-3.2-2.63-3.2-4.41c0-1.78,1.42-3.2,3.2-3.2h10.53c2.58,0,4.88,1.07,6.57,2.75c1.69,1.69,2.75,4.04,2.75,6.57v92.06 c0,2.58-1.07,4.88-2.75,6.57c-1.69,1.69-4.04,2.75-6.57,2.75H9.33c-2.58,0-4.88-1.07-6.57-2.75C1.07,118.44,0,116.08,0,113.55 V21.49c0-2.58,1.07-4.88,2.75-6.57c1.69-1.69,4.04-2.75,6.57-2.75h11.28c1.78,0,3.2,1.42,3.2,3.2s-1.42,4.41-3.2,4.41H10.54 c-0.8,0-1.55,0.31-2.09,0.84c-0.53,0.53-0.84,1.24-0.84,2.09v21.43L7.56,44.09L7.56,44.09z M115.19,52.9H7.56v59.4 c0,0.8,0.31,1.55,0.84,2.09c0.53,0.53,1.24,0.84,2.09,0.84h101.76c0.8,0,1.55-0.31,2.09-0.84c0.53-0.53,0.84-1.24,0.84-2.09V52.9 L115.19,52.9z M50.36,19.73c-1.78,0-3.2-2.63-3.2-4.41c0-1.78,1.42-3.2,3.2-3.2h21.49c1.78,0,3.2,1.42,3.2,3.2 c0,1.78-1.42,4.41-3.2,4.41H50.36L50.36,19.73z"/>
|
||||
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99999 15.8333C13.2217 15.8333 15.8333 13.2216 15.8333 9.99996C15.8333 6.7783 13.2217 4.16663 9.99999 4.16663C6.77833 4.16663 4.16666 6.7783 4.16666 9.99996C4.16666 13.2216 6.77833 15.8333 9.99999 15.8333Z" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.16666 10H15.8333" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.0002 4.16663C11.4593 5.764 12.2884 7.83698 12.3335 9.99996C12.2884 12.1629 11.4593 14.2359 10.0002 15.8333C8.54109 14.2359 7.7119 12.1629 7.66684 9.99996C7.7119 7.83698 8.54109 5.764 10.0002 4.16663V4.16663Z" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 831 B |
Binary file not shown.
Binary file not shown.
@@ -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}",,,
|
||||
,,,,,,,
|
||||
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
# Welcome to AppFlowy!
|
||||
|
||||
## Here are the basics
|
||||
|
||||
- [ ] Click anywhere and just start typing.
|
||||
- [ ] Highlight any text, and use the editing menu to _style_ **your** <u>writing</u> `however` you ~~like.~~
|
||||
- [ ] As soon as you type /a menu will pop up. Select different types of content blocks you can add.
|
||||
- [ ] Type `/` followed by `/bullet` or `/num` to create a list.
|
||||
- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page.
|
||||
- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
|
||||
|
||||
---
|
||||
|
||||
## Keyboard shortcuts, markdown, and code block
|
||||
|
||||
1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts)
|
||||
1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown)
|
||||
1. Type `/code` to insert a code block
|
||||
|
||||
## Have a question❓
|
||||
|
||||
> Click `?` at the bottom right for help and support.
|
||||
@@ -0,0 +1 @@
|
||||
# test
|
||||
@@ -51,6 +51,13 @@
|
||||
"import": "Import",
|
||||
"moreOptions": "More options"
|
||||
},
|
||||
"importPanel": {
|
||||
"textAndMarkdown": "Text & Markdown",
|
||||
"documentFromV010": "Document from v0.1.0",
|
||||
"databaseFromV010": "Database from v0.1.0",
|
||||
"csv": "CSV",
|
||||
"database": "Database"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
@@ -216,6 +223,7 @@
|
||||
"changeLocationTooltips": "Change the data directory",
|
||||
"change": "Change",
|
||||
"openLocationTooltips": "Open another data directory",
|
||||
"openCurrentDataFolder": "Open current data directory",
|
||||
"recoverLocationTooltips": "Reset to AppFlowy's default data directory",
|
||||
"exportFileSuccess": "Export file successfully!",
|
||||
"exportFileFail": "Export file failed!",
|
||||
@@ -229,6 +237,8 @@
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"deleteView": "Are you sure you want to delete this view?",
|
||||
"createView": "New",
|
||||
"settings": {
|
||||
"filter": "Filter",
|
||||
"sort": "Sort",
|
||||
@@ -368,11 +378,16 @@
|
||||
"grid": {
|
||||
"selectAGridToLinkTo": "Select a Grid to link to",
|
||||
"createANewGrid": "Create a new Grid"
|
||||
},
|
||||
"calendar": {
|
||||
"selectACalendarToLinkTo": "Select a Calendar to link to",
|
||||
"createANewCalendar": "Create a new Calendar"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"referencedBoard": "Referenced Board",
|
||||
"referencedGrid": "Referenced Grid",
|
||||
"referencedCalendar": "Referenced Calendar",
|
||||
"autoGeneratorMenuItemName": "OpenAI Writer",
|
||||
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
|
||||
"autoGeneratorLearnMore": "Learn more",
|
||||
@@ -459,7 +474,14 @@
|
||||
"layoutDateField": "Layout calendar by",
|
||||
"noDateTitle": "No Date",
|
||||
"noDateHint": "Unscheduled events will show up here",
|
||||
"clickToAdd": "Click to add to the calendar"
|
||||
}
|
||||
"clickToAdd": "Click to add to the calendar",
|
||||
"name": "Calendar layout"
|
||||
},
|
||||
"referencedCalendarPrefix": "View of"
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "AppFlowy Error",
|
||||
"howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.",
|
||||
"github": "View on GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": "Вид"
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,7 @@ void main() {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
|
||||
testWidgets(
|
||||
'integration test unzips the proper workspace and loads it correctly.',
|
||||
(tester) async {
|
||||
testWidgets('open the board with data structure in v0.2.0', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
expect(find.byType(AppFlowyBoard), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
/// Integration tests for an empty board. The [TestWorkspaceService] will load
|
||||
/// a workspace from an empty board `assets/test/workspaces/board.zip` for all
|
||||
/// tests.
|
||||
///
|
||||
/// To create another integration test with a preconfigured workspace.
|
||||
/// Use the following steps.
|
||||
/// 1. Create a new workspace from the AppFlowy launch screen.
|
||||
/// 2. Modify the workspace until it is suitable as the starting point for
|
||||
/// the integration test you need to land.
|
||||
/// 3. Use a zip utility program to zip the workspace folder that you created.
|
||||
/// 4. Add the zip file under `assets/test/workspaces/`
|
||||
/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`.
|
||||
/// For example, if you added a workspace called `empty_calendar.zip`,
|
||||
/// then [TestWorkspace] should have the following value:
|
||||
/// ```dart
|
||||
/// enum TestWorkspace {
|
||||
/// board('board'),
|
||||
/// empty_calendar('empty_calendar');
|
||||
///
|
||||
/// /* code */
|
||||
/// }
|
||||
/// ```
|
||||
/// 6. Double check that the .zip file that you added is included as an asset in
|
||||
/// the pubspec.yaml file under appflowy_flutter.
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
const service = TestWorkspaceService(TestWorkspace.coverImage);
|
||||
|
||||
group('cover image', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
|
||||
testWidgets(
|
||||
'hovering on cover image will display change and delete cover image buttons',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
expect(find.byType(Image), findsOneWidget);
|
||||
|
||||
final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse);
|
||||
final imageFinder = find.byType(Image);
|
||||
final Offset offset = tester.getCenter(imageFinder);
|
||||
|
||||
pointer.hover(offset);
|
||||
expect(find.byType(RoundedTextButton), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('calendar start from day setting', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create calendar view
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar');
|
||||
|
||||
// Open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapCalendarLayoutSettingButton();
|
||||
|
||||
// select the first day of week is Monday
|
||||
await tester.tapFirstDayOfWeek();
|
||||
await tester.tapFirstDayOfWeekStartFromMonday();
|
||||
|
||||
// Open the other page and open the new calendar page again
|
||||
await tester.openPage(readme);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
await tester.openPage('calendar');
|
||||
|
||||
// Open setting again and check the start from Monday is selected
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapCalendarLayoutSettingButton();
|
||||
await tester.tapFirstDayOfWeek();
|
||||
tester.assertFirstDayOfWeekStartFromMonday();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid 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();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
input: 'hello world',
|
||||
);
|
||||
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
content: 'hello world',
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit number cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
const fieldType = FieldType.Number;
|
||||
|
||||
// Create a number field
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
input: '-1',
|
||||
);
|
||||
// edit the next cell to force the previous cell at row 0 to lose focus
|
||||
await tester.editCell(
|
||||
rowIndex: 1,
|
||||
fieldType: fieldType,
|
||||
input: '0.2',
|
||||
);
|
||||
// -1 -> -1
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: '-1',
|
||||
);
|
||||
|
||||
// edit the next cell to force the previous cell at row 1 to lose focus
|
||||
await tester.editCell(
|
||||
rowIndex: 2,
|
||||
fieldType: fieldType,
|
||||
input: '.1',
|
||||
);
|
||||
// 0.2 -> 0.2
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 1,
|
||||
fieldType: fieldType,
|
||||
content: '0.2',
|
||||
);
|
||||
|
||||
// edit the next cell to force the previous cell at row 2 to lose focus
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
input: '',
|
||||
);
|
||||
// .1 -> 0.1
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 2,
|
||||
fieldType: fieldType,
|
||||
content: '0.1',
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit checkbox cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
await tester.assertCheckboxCell(rowIndex: 0, isSelected: false);
|
||||
await tester.tapCheckboxCellInGrid(rowIndex: 0);
|
||||
await tester.assertCheckboxCell(rowIndex: 0, isSelected: true);
|
||||
|
||||
await tester.tapCheckboxCellInGrid(rowIndex: 1);
|
||||
await tester.tapCheckboxCellInGrid(rowIndex: 2);
|
||||
await tester.assertCheckboxCell(rowIndex: 1, isSelected: true);
|
||||
await tester.assertCheckboxCell(rowIndex: 2, isSelected: true);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit create time cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
const fieldType = FieldType.CreatedTime;
|
||||
// Create a create time field
|
||||
// The create time field is not editable
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
|
||||
await tester.findDateEditor(findsNothing);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit last time cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
const fieldType = FieldType.LastEditedTime;
|
||||
// Create a last time field
|
||||
// The last time field is not editable
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
|
||||
await tester.findDateEditor(findsNothing);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit time cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
const fieldType = FieldType.DateTime;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
// Tap the cell to invoke the field editor
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Select the date
|
||||
await tester.selectDay(content: 3);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit single select cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
const fieldType = FieldType.SingleSelect;
|
||||
await tester.tapAddButton();
|
||||
// When create a grid, it will create a single select field by default
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Tap the cell to invoke the selection option editor
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
await tester.createOption(name: 'hello world');
|
||||
await tester.dismissSelectOptionEditor();
|
||||
|
||||
// Make sure the option is created and displayed in the cell
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'hello world',
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid page', () {
|
||||
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();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Invoke the field editor
|
||||
await tester.tapGridFieldWithName('Name');
|
||||
await tester.tapEditPropertyButton();
|
||||
|
||||
await tester.renameField('hello world');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
await tester.tapGridFieldWithName('hello world');
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('update field type of existing field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Invoke the field editor
|
||||
await tester.tapGridFieldWithName('Type');
|
||||
await tester.tapEditPropertyButton();
|
||||
|
||||
await tester.tapTypeOptionButton();
|
||||
await tester.selectFieldType(FieldType.Checkbox);
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
await tester.assertFieldTypeWithFieldName(
|
||||
'Type',
|
||||
FieldType.Checkbox,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create a field and rename it', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// create a field
|
||||
await tester.createField(FieldType.Checklist, 'checklist');
|
||||
|
||||
// check the field is created successfully
|
||||
await tester.findFieldWithName('checklist');
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('delete field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// create a field
|
||||
await tester.createField(FieldType.Checkbox, 'New field 1');
|
||||
|
||||
// Delete the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapDeletePropertyButton();
|
||||
|
||||
// confirm delete
|
||||
await tester.tapDialogOkButton();
|
||||
|
||||
await tester.noFieldWithName('New field 1');
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('duplicate field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// Delete the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapDuplicatePropertyButton();
|
||||
|
||||
await tester.findFieldWithName('New field 1 (copy)');
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('hide field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// Delete the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapHidePropertyButton();
|
||||
|
||||
await tester.noFieldWithName('New field 1');
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create checklist field ', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
|
||||
// Open the type option menu
|
||||
await tester.tapTypeOptionButton();
|
||||
|
||||
await tester.selectFieldType(FieldType.Checklist);
|
||||
|
||||
// After update the field type, the cells should be updated
|
||||
await tester.findCellByFieldType(FieldType.Checklist);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create list of fields', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
for (final fieldType in [
|
||||
FieldType.Checklist,
|
||||
FieldType.DateTime,
|
||||
FieldType.Number,
|
||||
FieldType.URL,
|
||||
FieldType.MultiSelect,
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Checkbox,
|
||||
]) {
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField(fieldType.name);
|
||||
|
||||
// Open the type option menu
|
||||
await tester.tapTypeOptionButton();
|
||||
|
||||
await tester.selectFieldType(fieldType);
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// After update the field type, the cells should be updated
|
||||
await tester.findCellByFieldType(fieldType);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'import_files';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('add text filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name');
|
||||
await tester.tapFilterButtonInGrid('Name');
|
||||
|
||||
// enter 'A' in the filter text field
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
await tester.enterTextInTextFilter('A');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// after remove the filter, the grid should show all rows
|
||||
await tester.enterTextInTextFilter('');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.enterTextInTextFilter('B');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// open the menu to delete the filter
|
||||
await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checkbox filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Done');
|
||||
await tester.tapCheckboxFilterButtonInGrid();
|
||||
|
||||
await tester.tapUnCheckedButtonOnCheckboxFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
|
||||
await tester
|
||||
.tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));
|
||||
await tester.tapDeleteFilterButtonInGrid();
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checklist filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');
|
||||
|
||||
// By default, the condition of checklist filter is 'uncompleted'
|
||||
await tester.assertNumberOfRowsInGridPage(9);
|
||||
|
||||
await tester.tapFilterButtonInGrid('checklist');
|
||||
await tester.tapChecklistFilterButtonInGrid();
|
||||
|
||||
await tester.tapCompletedButtonOnChecklistFilter();
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add single select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type');
|
||||
|
||||
await tester.tapFilterButtonInGrid('Type');
|
||||
|
||||
// select the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
// unselect the option 's6'
|
||||
await tester.tapOptionFilterWithName('s6');
|
||||
await tester.assertNumberOfRowsInGridPage(10);
|
||||
|
||||
// select the option 's5'
|
||||
await tester.tapOptionFilterWithName('s5');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
// select the option 's4'
|
||||
await tester.tapOptionFilterWithName('s4');
|
||||
|
||||
// The row with 's4' or 's5' should be shown.
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add multi select filter', (tester) async {
|
||||
await tester.openV020database();
|
||||
|
||||
// create a filter
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(
|
||||
FieldType.MultiSelect,
|
||||
'multi-select',
|
||||
);
|
||||
|
||||
await tester.tapFilterButtonInGrid('multi-select');
|
||||
await tester.scrollOptionFilterListByOffset(const Offset(0, -200));
|
||||
|
||||
// select the option 'm1'. Any option with 'm1' should be shown.
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
await tester.assertNumberOfRowsInGridPage(5);
|
||||
await tester.tapOptionFilterWithName('m1');
|
||||
|
||||
// select the option 'm2'. Any option with 'm2' should be shown.
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.tapOptionFilterWithName('m2');
|
||||
|
||||
// select the option 'm4'. Any option with 'm4' should be shown.
|
||||
await tester.tapOptionFilterWithName('m4');
|
||||
await tester.assertNumberOfRowsInGridPage(1);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/ime.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
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('insert emoji in the row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
await tester.hoverRowBanner();
|
||||
|
||||
await tester.openEmojiPicker();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// After select the emoji, the EmojiButton will show up
|
||||
await tester.tapButton(find.byType(EmojiButton));
|
||||
});
|
||||
|
||||
testWidgets('update emoji in the row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.hoverRowBanner();
|
||||
await tester.openEmojiPicker();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// Update existing selected emoji
|
||||
await tester.tapButton(find.byType(EmojiButton));
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😅');
|
||||
|
||||
// The emoji already displayed in the row banner
|
||||
final emojiText = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == '😅',
|
||||
);
|
||||
|
||||
// The number of emoji should be two. One in the row displayed in the grid
|
||||
// one in the row detail page.
|
||||
expect(emojiText, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('remove emoji in the row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.hoverRowBanner();
|
||||
await tester.openEmojiPicker();
|
||||
await tester.switchToEmojiList();
|
||||
await tester.tapEmoji('😀');
|
||||
|
||||
// Remove the emoji
|
||||
await tester.tapButton(find.byType(RemoveEmojiButton));
|
||||
final emojiText = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == '😀',
|
||||
);
|
||||
expect(emojiText, findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('create list of fields in row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
for (final fieldType in [
|
||||
FieldType.Checklist,
|
||||
FieldType.DateTime,
|
||||
FieldType.Number,
|
||||
FieldType.URL,
|
||||
FieldType.MultiSelect,
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Checkbox,
|
||||
]) {
|
||||
await tester.tapRowDetailPageCreatePropertyButton();
|
||||
await tester.renameField(fieldType.name);
|
||||
|
||||
// Open the type option menu
|
||||
await tester.tapTypeOptionButton();
|
||||
|
||||
await tester.selectFieldType(fieldType);
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// After update the field type, the cells should be updated
|
||||
await tester.findCellByFieldType(fieldType);
|
||||
await tester.scrollRowDetailByOffset(const Offset(0, -50));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('check document is exist in row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Each row detail page should have a document
|
||||
await tester.assertDocumentExistInRowDetailPage();
|
||||
});
|
||||
|
||||
testWidgets('update the content of the document and re-open it',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Wait for the document to be loaded
|
||||
await tester.wait(500);
|
||||
|
||||
// Focus on the editor
|
||||
final textBlock = find.byType(TextBlockComponentWidget);
|
||||
await tester.tapAt(tester.getCenter(textBlock));
|
||||
|
||||
// Input some text
|
||||
const inputText = 'Hello world';
|
||||
await tester.ime.insertText(inputText);
|
||||
expect(
|
||||
find.textContaining(inputText, findRichText: true),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Tap outside to dismiss the field
|
||||
await tester.tapAt(Offset.zero);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Re-open the document
|
||||
await tester.openFirstRowDetailPage();
|
||||
expect(
|
||||
find.textContaining(inputText, findRichText: true),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('delete row in row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
});
|
||||
|
||||
testWidgets('duplicate row in row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = '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();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
await tester.tapCreateRowButtonInGrid();
|
||||
|
||||
// The initial number of rows is 3
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create row from row menu of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
|
||||
await tester.tapCreateRowButtonInRowMenuOfGrid();
|
||||
|
||||
// The initial number of rows is 3
|
||||
await tester.assertNumberOfRowsInGridPage(4);
|
||||
await tester.assertRowCountInGridPage(4);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('delete row of the grid', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
|
||||
// Open the row menu and then click the delete
|
||||
await tester.tapRowMenuButtonInGrid();
|
||||
await tester.tapDeleteOnRowMenu();
|
||||
|
||||
// The initial number of rows is 3
|
||||
await tester.assertNumberOfRowsInGridPage(2);
|
||||
await tester.assertRowCountInGridPage(2);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('check number of row indicator in the initial grid',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
await tester.assertRowCountInGridPage(3);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
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();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
// select the layout
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
// select layout by board
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('update layout multiple times', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// open setting
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapDatabaseLayoutButton();
|
||||
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Calendar);
|
||||
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'import_files';
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('import v0.2.0 database data', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapImportButton();
|
||||
|
||||
final testFileNames = ['v020.afdb'];
|
||||
final fileLocation = await tester.currentFileLocation();
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/database',
|
||||
fileName,
|
||||
),
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: location);
|
||||
await tester.tapDatabaseRawDataButton();
|
||||
await tester.openPage('v020');
|
||||
|
||||
// check the import content
|
||||
// await tester.assertCellContent(
|
||||
// rowIndex: 7,
|
||||
// fieldType: FieldType.RichText,
|
||||
// // fieldName: 'Name',
|
||||
// content: '',
|
||||
// );
|
||||
|
||||
// check the text cell
|
||||
final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];
|
||||
for (final (index, content) in textCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the checkbox cell
|
||||
final checkboxCells = <bool>[
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
];
|
||||
for (final (index, content) in checkboxCells.indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the number cell
|
||||
final numberCells = <String>[
|
||||
'-1',
|
||||
'-2',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
'2',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
''
|
||||
];
|
||||
for (final (index, content) in numberCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the url cell
|
||||
final urlCells = <String>[
|
||||
'appflowy.io',
|
||||
'no url',
|
||||
'appflowy.io',
|
||||
'https://github.com/AppFlowy-IO/',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in urlCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.URL,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the single select cell
|
||||
final singleSelectCells = <String>[
|
||||
's1',
|
||||
's2',
|
||||
's3',
|
||||
's4',
|
||||
's5',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in singleSelectCells.indexed) {
|
||||
await tester.assertSingleSelectOption(
|
||||
rowIndex: index,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the multi select cell
|
||||
final List<List<String>> multiSelectCells = [
|
||||
['m1'],
|
||||
['m1', 'm2'],
|
||||
['m1', 'm2', 'm3'],
|
||||
['m1', 'm2', 'm3'],
|
||||
['m1', 'm2', 'm3', 'm4', 'm5'],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
];
|
||||
for (final (index, contents) in multiSelectCells.indexed) {
|
||||
await tester.assertMultiSelectOption(
|
||||
rowIndex: index,
|
||||
contents: contents,
|
||||
);
|
||||
}
|
||||
|
||||
// check the checklist cell
|
||||
final List<double> checklistCells = [
|
||||
0.6,
|
||||
0.3,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
];
|
||||
for (final (index, percent) in checklistCells.indexed) {
|
||||
await tester.assertChecklistCellInGrid(
|
||||
rowIndex: index,
|
||||
percent: percent,
|
||||
);
|
||||
}
|
||||
|
||||
// check the date cell
|
||||
final List<String> dateCells = [
|
||||
'Jun 01, 2023',
|
||||
'Jun 02, 2023',
|
||||
'Jun 03, 2023',
|
||||
'Jun 04, 2023',
|
||||
'Jun 05, 2023',
|
||||
'Jun 05, 2023',
|
||||
'Jun 16, 2023',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
];
|
||||
for (final (index, content) in dateCells.indexed) {
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.DateTime,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid', () {
|
||||
const location = 'import_files';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('add text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');
|
||||
|
||||
// check the text cell order
|
||||
final textCells = <String>[
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
];
|
||||
for (final (index, content) in textCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('Name');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'E',
|
||||
'D',
|
||||
'C',
|
||||
'B',
|
||||
'A',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// delete all sorts
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapAllSortButton();
|
||||
|
||||
// check the text cell order
|
||||
for (final (index, content) in <String>[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add checkbox sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
|
||||
// check the checkbox cell order
|
||||
for (final (index, content) in <bool>[
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('Done');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <bool>[
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add number sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
|
||||
// check the number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'',
|
||||
'-2',
|
||||
'-1',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
'2',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// open the sort menu and select order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('number');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('add number and text sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
|
||||
// open the sort menu and select number order by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('number');
|
||||
await tester.tapSortByDescending();
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapCreateSortByFieldTypeInSortMenu(
|
||||
FieldType.RichText,
|
||||
'Name',
|
||||
);
|
||||
|
||||
// check number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'',
|
||||
'-1',
|
||||
'-2',
|
||||
'0.1',
|
||||
'0.2',
|
||||
'1',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check text cell order
|
||||
for (final (index, content) in <String>[
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
].indexed) {
|
||||
await tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.RichText,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('create linked view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// Create grid view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
|
||||
|
||||
// Create calendar view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('rename and delete linked view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// rename board view
|
||||
await tester.renameLinkedView(
|
||||
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
|
||||
'new board',
|
||||
);
|
||||
final findBoard = tester.findTabBarLinkViewByViewName('new board');
|
||||
expect(findBoard, findsOneWidget);
|
||||
|
||||
// delete the board
|
||||
await tester.deleteDatebaseView(findBoard);
|
||||
expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('delete the last database view', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// delete the board
|
||||
await tester.deleteDatebaseView(
|
||||
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database view in document', () {
|
||||
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();
|
||||
|
||||
await insertReferenceDatabase(tester, ViewLayoutPB.Grid);
|
||||
|
||||
// validate the referenced grid is inserted
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(GridPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('insert a referenced board', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await insertReferenceDatabase(tester, ViewLayoutPB.Board);
|
||||
|
||||
// validate the referenced board is inserted
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(BoardPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
// testWidgets('insert a referenced calendar', (tester) async {
|
||||
// await tester.initializeAppFlowy();
|
||||
// await tester.tapGoButton();
|
||||
|
||||
// await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
||||
|
||||
// // validate the referenced grid is inserted
|
||||
// expect(
|
||||
// find.descendant(
|
||||
// of: find.byType(AppFlowyEditor),
|
||||
// matching: find.byType(CalendarPage),
|
||||
// ),
|
||||
// findsOneWidget,
|
||||
// );
|
||||
// });
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert a referenced database of [layout] into the document
|
||||
Future<void> insertReferenceDatabase(
|
||||
WidgetTester tester,
|
||||
ViewLayoutPB layout,
|
||||
) async {
|
||||
// create a new grid
|
||||
final id = uuid();
|
||||
final name = '${layout.name}_$id';
|
||||
await tester.createNewPageWithName(
|
||||
layout,
|
||||
name,
|
||||
);
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
ViewLayoutPB.Document,
|
||||
'insert_a_reference_${layout.name}',
|
||||
);
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a referenced grid
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
layout.referencedMenuName,
|
||||
);
|
||||
|
||||
final linkToPageMenu = find.byType(LinkToPageMenu);
|
||||
expect(linkToPageMenu, findsOneWidget);
|
||||
final referencedDatabase = find.descendant(
|
||||
of: linkToPageMenu,
|
||||
matching: find.findTextInFlowyText(name),
|
||||
);
|
||||
expect(referencedDatabase, findsOneWidget);
|
||||
await tester.tapButton(referencedDatabase);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/ime.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('edit document', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('redo & undo', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document called Sample
|
||||
const pageName = 'Sample';
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
|
||||
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert 1. to trigger it to be a numbered list
|
||||
await tester.ime.insertText('1. ');
|
||||
expect(find.text('1.', findRichText: true), findsOneWidget);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
|
||||
// undo
|
||||
// numbered list will be reverted to paragraph
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
ParagraphBlockKeys.type,
|
||||
);
|
||||
|
||||
// redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
|
||||
// switch to other page and switch back
|
||||
await tester.openPage(readme);
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// the numbered list should be kept
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,
|
||||
NumberedListBlockKeys.type,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('write a readme document', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document called Sample
|
||||
const pageName = 'Sample';
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
|
||||
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// mock inputting the sample
|
||||
final lines = _sample.split('\n');
|
||||
for (final line in lines) {
|
||||
await tester.ime.insertText(line);
|
||||
await tester.ime.insertCharacter('\n');
|
||||
}
|
||||
|
||||
// switch to other page and switch back
|
||||
await tester.openPage(readme);
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// this screenshots are different on different platform, so comment it out temporarily.
|
||||
// check the document
|
||||
// await expectLater(
|
||||
// find.byType(AppFlowyEditor),
|
||||
// matchesGoldenFile('document/edit_document_test.png'),
|
||||
// );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet.
|
||||
const _sample = r'''
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
---
|
||||
[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~
|
||||
|
||||
[] Type / followed by /bullet or /num to create a list.
|
||||
|
||||
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
|
||||
|
||||
[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
|
||||
---
|
||||
* bulleted list 1
|
||||
|
||||
* bulleted list 2
|
||||
|
||||
* bulleted list 3
|
||||
bulleted list 4
|
||||
---
|
||||
1. numbered list 1
|
||||
|
||||
2. numbered list 2
|
||||
|
||||
3. numbered list 3
|
||||
numbered list 4
|
||||
---
|
||||
" quote''';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,60 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('import files', () {
|
||||
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();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapImportButton();
|
||||
|
||||
final testFileNames = ['test1.md', 'test2.md'];
|
||||
final fileLocation = await tester.currentFileLocation();
|
||||
for (final fileName in testFileNames) {
|
||||
final str = await rootBundle.loadString(
|
||||
p.join(
|
||||
'assets/test/workspaces/markdowns',
|
||||
fileName,
|
||||
),
|
||||
);
|
||||
File(p.join(fileLocation, fileName)).writeAsStringSync(str);
|
||||
}
|
||||
// mock get files
|
||||
await mockPickFilePaths(testFileNames, name: location);
|
||||
|
||||
await tester.tapTextAndMarkdownButton();
|
||||
|
||||
tester.expectToSeePageName('test1');
|
||||
tester.expectToSeePageName('test2');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document', () {
|
||||
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 {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapLanguageSelectorOnWelcomePage();
|
||||
expect(find.byType(LanguageItemsListView), findsOneWidget);
|
||||
|
||||
await tester.tapLanguageItem(languageCode: 'zh', countryCode: 'CN');
|
||||
tester.expectToSeeText('开始');
|
||||
|
||||
await tester.tapLanguageItem(languageCode: 'en', scrollDelta: -100);
|
||||
tester.expectToSeeText('Quick Start');
|
||||
|
||||
await tester.tapLanguageItem(languageCode: 'it', countryCode: 'IT');
|
||||
tester.expectToSeeText('Andiamo');
|
||||
});
|
||||
|
||||
/// Make sure this test is executed after the test above.
|
||||
testWidgets('check the language after relaunching the app', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
tester.expectToSeeText('Andiamo');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'board_test.dart' as board_test;
|
||||
import 'switch_folder_test.dart' as switch_folder_test;
|
||||
import 'empty_document_test.dart' as empty_document_test;
|
||||
import 'open_ai_smart_menu_test.dart' as smart_menu_test;
|
||||
import 'document_test.dart' as document_test;
|
||||
import 'document/document_test.dart' as document_test;
|
||||
import 'document/cover_image_test.dart' as cover_image_test;
|
||||
import 'share_markdown_test.dart' as share_markdown_test;
|
||||
import 'import_files_test.dart' as import_files_test;
|
||||
import 'document/document_with_database_test.dart'
|
||||
as document_with_database_test;
|
||||
import 'document/edit_document_test.dart' as edit_document_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_row_test.dart' as database_row_test;
|
||||
import 'database_setting_test.dart' as database_setting_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_view_test.dart' as database_view_test;
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_sort_test.dart' as database_sort_test;
|
||||
|
||||
/// The main task runner for all integration tests in AppFlowy.
|
||||
///
|
||||
@@ -16,8 +29,28 @@ import 'document_test.dart' as document_test;
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
switch_folder_test.main();
|
||||
board_test.main();
|
||||
empty_document_test.main();
|
||||
smart_menu_test.main();
|
||||
share_markdown_test.main();
|
||||
import_files_test.main();
|
||||
|
||||
// Document integration tests
|
||||
cover_image_test.main();
|
||||
document_test.main();
|
||||
document_with_database_test.main();
|
||||
edit_document_test.main();
|
||||
|
||||
// Database integration tests
|
||||
database_cell_test.main();
|
||||
database_field_test.main();
|
||||
database_share_test.main();
|
||||
database_row_page_test.main();
|
||||
database_row_test.main();
|
||||
database_setting_test.main();
|
||||
database_filter_test.main();
|
||||
database_sort_test.main();
|
||||
database_view_test.main();
|
||||
database_calendar_test.main();
|
||||
|
||||
// board_test.main();
|
||||
// empty_document_test.main();
|
||||
// smart_menu_test.main();
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
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();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
// mock the file picker
|
||||
final path = await mockSaveFilePath(location, 'test.md');
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
await tester.tapMarkdownButton();
|
||||
|
||||
// expect to see the success dialog
|
||||
tester.expectToExportSuccess();
|
||||
|
||||
final file = File(path);
|
||||
final isExist = file.existsSync();
|
||||
expect(isExist, true);
|
||||
final markdown = file.readAsStringSync();
|
||||
expect(markdown, expectedMarkdown);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'share the markdown after renaming the document name',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// expect to see a readme page
|
||||
tester.expectToSeePageName(readme);
|
||||
|
||||
// rename the document
|
||||
await tester.hoverOnPageName(readme);
|
||||
await tester.renamePage('example');
|
||||
|
||||
final shareButton = find.byType(ShareActionList);
|
||||
final shareButtonState =
|
||||
tester.state(shareButton) as ShareActionListState;
|
||||
final path =
|
||||
await mockSaveFilePath(location, '${shareButtonState.name}.md');
|
||||
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
await tester.tapMarkdownButton();
|
||||
|
||||
// expect to see the success dialog
|
||||
tester.expectToExportSuccess();
|
||||
|
||||
final file = File(path);
|
||||
final isExist = file.existsSync();
|
||||
expect(isExist, true);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const expectedMarkdown = r'''
|
||||
# Welcome to AppFlowy!
|
||||
## Here are the basics
|
||||
- [ ] Click anywhere and just start typing.
|
||||
- [ ] Highlight any text, and use the editing menu to _style_ **your** <u>writing</u> `however` you ~~like.~~
|
||||
- [ ] As soon as you type `/` a menu will pop up. Select different types of content blocks you can add.
|
||||
- [ ] Type `/` followed by `/bullet` or `/num` to create a list.
|
||||
- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page.
|
||||
- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
|
||||
|
||||
---
|
||||
|
||||
## Keyboard shortcuts, markdown, and code block
|
||||
1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts)
|
||||
1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown)
|
||||
1. Type `/code` to insert a code block
|
||||
|
||||
## Have a question❓
|
||||
> Click `?` at the bottom right for help and support.
|
||||
|
||||
|
||||
|
||||
''';
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:appflowy/user/presentation/folder/folder_widget.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -25,69 +27,22 @@ void main() {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'customize folder name and path when launching app in first time',
|
||||
(tester) async {
|
||||
const folderName = 'appflowy';
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
// Click create button
|
||||
await tester.tapCreateButton();
|
||||
|
||||
// Set directory
|
||||
final cfw = find.byType(CreateFolderWidget);
|
||||
expect(cfw, findsOneWidget);
|
||||
final state = tester.state(cfw) as CreateFolderWidgetState;
|
||||
final dir = await TestFolder.testLocation(null);
|
||||
state.directory = dir.path;
|
||||
|
||||
// input folder name
|
||||
final ftf = find.byType(FlowyTextField);
|
||||
expect(ftf, findsOneWidget);
|
||||
await tester.enterText(ftf, 'appflowy');
|
||||
|
||||
// Click create button again
|
||||
await tester.tapCreateButton();
|
||||
|
||||
tester.expectToSeeWelcomePage();
|
||||
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
});
|
||||
|
||||
testWidgets('open a new folder when launching app in first time',
|
||||
(tester) async {
|
||||
const folderName = 'appflowy';
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
await TestFolder.setTestLocation(folderName);
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
// tap open button
|
||||
await mockGetDirectoryPath(folderName);
|
||||
await tester.tapOpenFolderButton();
|
||||
|
||||
await tester.wait(1000);
|
||||
tester.expectToSeeWelcomePage();
|
||||
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
});
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
const String userA = 'userA';
|
||||
const String userB = 'userB';
|
||||
final userA = uuid();
|
||||
final userB = uuid();
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.setTestLocation(userA);
|
||||
await TestFolder.cleanTestLocation(userB);
|
||||
await TestFolder.setTestLocation(p.join(userA, appFlowyDataFolder));
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeWelcomePage();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
// switch to user B
|
||||
{
|
||||
// set user name to userA
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userA);
|
||||
@@ -99,15 +54,16 @@ void main() {
|
||||
await mockGetDirectoryPath(userB);
|
||||
await tester.tapCustomLocationButton();
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeWelcomePage();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
// set user name to userB
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userB);
|
||||
}
|
||||
|
||||
// switch to the userA
|
||||
{
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userB);
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -116,8 +72,8 @@ void main() {
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeWelcomePage();
|
||||
expect(find.textContaining(userA), findsOneWidget);
|
||||
tester.expectToSeeHomePage();
|
||||
tester.expectToSeeUserName(userA);
|
||||
}
|
||||
|
||||
// switch to the userB again
|
||||
@@ -131,8 +87,8 @@ void main() {
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeWelcomePage();
|
||||
expect(find.textContaining(userB), findsOneWidget);
|
||||
tester.expectToSeeHomePage();
|
||||
tester.expectToSeeUserName(userB);
|
||||
}
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
@@ -145,7 +101,7 @@ void main() {
|
||||
await tester.tapGoButton();
|
||||
|
||||
// home and readme document
|
||||
tester.expectToSeeWelcomePage();
|
||||
tester.expectToSeeHomePage();
|
||||
|
||||
// open settings and restore the location
|
||||
await tester.openSettings();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/main.dart' as app;
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -41,7 +44,7 @@ class TestFolder {
|
||||
|
||||
/// Get default location under development environment.
|
||||
static Future<String> defaultDevelopmentLocation() async {
|
||||
final dir = await appFlowyDocumentDirectory();
|
||||
final dir = await appFlowyApplicationDataDirectory();
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
@@ -59,18 +62,18 @@ class TestFolder {
|
||||
extension AppFlowyTestBase on WidgetTester {
|
||||
Future<void> initializeAppFlowy() async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('hotkey_manager'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
);
|
||||
return;
|
||||
});
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await FlowyRunner.run(FlowyApp(), IntegrationMode.integrationTest);
|
||||
|
||||
await app.main();
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
}
|
||||
@@ -82,7 +85,11 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
bool warnIfMissed = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(finder);
|
||||
await tap(
|
||||
finder,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
}
|
||||
@@ -91,7 +98,16 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
final button = find.text(tr, findRichText: true);
|
||||
Finder button = find.text(
|
||||
tr,
|
||||
findRichText: true,
|
||||
skipOffstage: false,
|
||||
);
|
||||
if (button.evaluate().isEmpty) {
|
||||
button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == tr,
|
||||
);
|
||||
}
|
||||
await tapButton(
|
||||
button,
|
||||
milliseconds: milliseconds,
|
||||
@@ -116,3 +132,11 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
extension AppFlowyFinderTestBase on CommonFinders {
|
||||
Finder findTextInFlowyText(String text) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == text,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import 'dart:ui';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
extension CommonOperations on WidgetTester {
|
||||
/// 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);
|
||||
await tapButton(goButton);
|
||||
}
|
||||
|
||||
/// Tap the + button on the home page.
|
||||
Future<void> tapAddButton() async {
|
||||
final addButton = find.byType(AddButton);
|
||||
await tapButton(addButton);
|
||||
}
|
||||
|
||||
/// Tap the create document button.
|
||||
///
|
||||
/// Must call [tapAddButton] first.
|
||||
Future<void> tapCreateDocumentButton() async {
|
||||
await tapButtonWithName(LocaleKeys.document_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the create grid button.
|
||||
///
|
||||
/// Must call [tapAddButton] first.
|
||||
Future<void> tapCreateGridButton() async {
|
||||
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the create grid button.
|
||||
///
|
||||
/// Must call [tapAddButton] first.
|
||||
Future<void> tapCreateCalendarButton() async {
|
||||
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the import button.
|
||||
///
|
||||
/// Must call [tapAddButton] first.
|
||||
Future<void> tapImportButton() async {
|
||||
await tapButtonWithName(LocaleKeys.moreAction_import.tr());
|
||||
}
|
||||
|
||||
/// Tap the import from text & markdown button.
|
||||
///
|
||||
/// Must call [tapImportButton] first.
|
||||
Future<void> tapTextAndMarkdownButton() async {
|
||||
await tapButtonWithName(LocaleKeys.importPanel_textAndMarkdown.tr());
|
||||
}
|
||||
|
||||
/// Tap the LanguageSelectorOnWelcomePage widget on the launch page.
|
||||
Future<void> tapLanguageSelectorOnWelcomePage() async {
|
||||
final languageSelector = find.byType(LanguageSelectorOnWelcomePage);
|
||||
await tapButton(languageSelector);
|
||||
}
|
||||
|
||||
/// Tap languageItem on LanguageItemsListView.
|
||||
///
|
||||
/// [scrollDelta] is the distance to scroll the ListView.
|
||||
/// Default value is 100
|
||||
///
|
||||
/// If it is positive -> scroll down.
|
||||
///
|
||||
/// If it is negative -> scroll up.
|
||||
Future<void> tapLanguageItem({
|
||||
required String languageCode,
|
||||
String? countryCode,
|
||||
double? scrollDelta,
|
||||
}) async {
|
||||
final languageItemsListView = find.descendant(
|
||||
of: find.byType(ListView),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
|
||||
final languageItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is LanguageItem &&
|
||||
widget.locale.languageCode == languageCode &&
|
||||
widget.locale.countryCode == countryCode,
|
||||
);
|
||||
|
||||
// scroll the ListView until zHCNLanguageItem shows on the screen.
|
||||
await scrollUntilVisible(
|
||||
languageItem,
|
||||
scrollDelta ?? 100,
|
||||
scrollable: languageItemsListView,
|
||||
// maxHeight of LanguageItemsListView
|
||||
maxScrolls: 400,
|
||||
);
|
||||
|
||||
try {
|
||||
await tapButton(languageItem);
|
||||
} on FlutterError catch (e) {
|
||||
Log.warn('tapLanguageItem error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Hover on the widget.
|
||||
Future<void> hoverOnWidget(
|
||||
Finder finder, {
|
||||
Offset? offset,
|
||||
Future<void> Function()? onHover,
|
||||
}) async {
|
||||
try {
|
||||
final gesture = await createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: Offset.zero);
|
||||
await pump();
|
||||
await gesture.moveTo(offset ?? getCenter(finder));
|
||||
await pumpAndSettle();
|
||||
await onHover?.call();
|
||||
await gesture.removePointer();
|
||||
} catch (err) {
|
||||
Log.error('hoverOnWidget error: $err');
|
||||
}
|
||||
}
|
||||
|
||||
/// Hover on the page name.
|
||||
Future<void> hoverOnPageName(
|
||||
String name, {
|
||||
Future<void> Function()? onHover,
|
||||
bool useLast = true,
|
||||
}) async {
|
||||
if (useLast) {
|
||||
await hoverOnWidget(findPageName(name).last, onHover: onHover);
|
||||
} else {
|
||||
await hoverOnWidget(findPageName(name).first, onHover: onHover);
|
||||
}
|
||||
}
|
||||
|
||||
/// open the page with given name.
|
||||
Future<void> openPage(String name) async {
|
||||
final finder = findPageName(name);
|
||||
expect(finder, findsOneWidget);
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
/// Tap the ... button beside the page name.
|
||||
///
|
||||
/// Must call [hoverOnPageName] first.
|
||||
Future<void> tapPageOptionButton() async {
|
||||
final optionButton = find.byType(ViewDisclosureButton);
|
||||
await tapButton(optionButton);
|
||||
}
|
||||
|
||||
/// Tap the delete page button.
|
||||
Future<void> tapDeletePageButton() async {
|
||||
await tapPageOptionButton();
|
||||
await tapButtonWithName(ViewDisclosureAction.delete.name);
|
||||
}
|
||||
|
||||
/// Tap the rename page button.
|
||||
Future<void> tapRenamePageButton() async {
|
||||
await tapPageOptionButton();
|
||||
await tapButtonWithName(ViewDisclosureAction.rename.name);
|
||||
}
|
||||
|
||||
/// Rename the page.
|
||||
Future<void> renamePage(String name) async {
|
||||
await tapRenamePageButton();
|
||||
await enterText(find.byType(TextFormField), name);
|
||||
await tapOKButton();
|
||||
}
|
||||
|
||||
Future<void> tapOKButton() async {
|
||||
final okButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PrimaryTextButton &&
|
||||
widget.label == LocaleKeys.button_OK.tr(),
|
||||
);
|
||||
await tapButton(okButton);
|
||||
}
|
||||
|
||||
/// Tap the restore button.
|
||||
///
|
||||
/// the restore button will show after the current page is deleted.
|
||||
Future<void> tapRestoreButton() async {
|
||||
final restoreButton = find.textContaining(
|
||||
LocaleKeys.deletePagePrompt_restore.tr(),
|
||||
);
|
||||
await tapButton(restoreButton);
|
||||
}
|
||||
|
||||
/// Tap the delete permanently button.
|
||||
///
|
||||
/// the restore button will show after the current page is deleted.
|
||||
Future<void> tapDeletePermanentlyButton() async {
|
||||
final restoreButton = find.textContaining(
|
||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
||||
);
|
||||
await tapButton(restoreButton);
|
||||
}
|
||||
|
||||
/// Tap the share button above the document page.
|
||||
Future<void> tapShareButton() async {
|
||||
final shareButton = find.byWidgetPredicate(
|
||||
(widget) => widget is DocumentShareButton,
|
||||
);
|
||||
await tapButton(shareButton);
|
||||
}
|
||||
|
||||
/// Tap the export markdown button
|
||||
///
|
||||
/// Must call [tapShareButton] first.
|
||||
Future<void> tapMarkdownButton() async {
|
||||
final markdownButton = find.textContaining(
|
||||
LocaleKeys.shareAction_markdown.tr(),
|
||||
);
|
||||
await tapButton(markdownButton);
|
||||
}
|
||||
|
||||
Future<void> createNewPageWithName(ViewLayoutPB layout, String name) async {
|
||||
// create a new page
|
||||
await tapAddButton();
|
||||
await tapButtonWithName(layout.menuName);
|
||||
await pumpAndSettle();
|
||||
|
||||
// hover on it and change it's name
|
||||
await hoverOnPageName(
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
onHover: () async {
|
||||
await renamePage(name);
|
||||
await pumpAndSettle();
|
||||
},
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> simulateKeyEvent(
|
||||
LogicalKeyboardKey key, {
|
||||
bool isControlPressed = false,
|
||||
bool isShiftPressed = false,
|
||||
bool isAltPressed = false,
|
||||
bool isMetaPressed = false,
|
||||
}) async {
|
||||
if (isControlPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.control);
|
||||
}
|
||||
if (isShiftPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.shift);
|
||||
}
|
||||
if (isAltPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.alt);
|
||||
}
|
||||
if (isMetaPressed) {
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.meta);
|
||||
}
|
||||
await simulateKeyDownEvent(key);
|
||||
await simulateKeyUpEvent(key);
|
||||
if (isControlPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.control);
|
||||
}
|
||||
if (isShiftPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.shift);
|
||||
}
|
||||
if (isAltPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.alt);
|
||||
}
|
||||
if (isMetaPressed) {
|
||||
await simulateKeyUpEvent(LogicalKeyboardKey.meta);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
String get menuName {
|
||||
switch (this) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return LocaleKeys.grid_menuName.tr();
|
||||
case ViewLayoutPB.Board:
|
||||
return LocaleKeys.board_menuName.tr();
|
||||
case ViewLayoutPB.Document:
|
||||
return LocaleKeys.document_menuName.tr();
|
||||
case ViewLayoutPB.Calendar:
|
||||
return LocaleKeys.calendar_menuName.tr();
|
||||
default:
|
||||
throw UnsupportedError('Unsupported layout: $this');
|
||||
}
|
||||
}
|
||||
|
||||
String get referencedMenuName {
|
||||
switch (this) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return LocaleKeys.document_plugins_referencedGrid.tr();
|
||||
case ViewLayoutPB.Board:
|
||||
return LocaleKeys.document_plugins_referencedBoard.tr();
|
||||
case ViewLayoutPB.Calendar:
|
||||
return LocaleKeys.document_plugins_referencedCalendar.tr();
|
||||
default:
|
||||
throw UnsupportedError('Unsupported layout: $this');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,11 @@ class TestWorkspaceService {
|
||||
|
||||
/// Instructs the application to read workspace data from the workspace found under this [TestWorkspace]'s path.
|
||||
Future<void> setUpAll() async {
|
||||
final root = await workspace.root;
|
||||
final path = root.path;
|
||||
SharedPreferences.setMockInitialValues(
|
||||
{
|
||||
KVKeys.pathLocation: await workspace.root.then((value) => value.path),
|
||||
KVKeys.pathLocation: path,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,51 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'ime.dart';
|
||||
import 'util.dart';
|
||||
|
||||
extension EditorWidgetTester on WidgetTester {
|
||||
EditorOperations get editor => EditorOperations(this);
|
||||
}
|
||||
|
||||
class EditorOperations {
|
||||
const EditorOperations(this.tester);
|
||||
|
||||
final WidgetTester tester;
|
||||
|
||||
EditorState getCurrentEditorState() {
|
||||
return tester
|
||||
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
|
||||
.editorState;
|
||||
}
|
||||
|
||||
/// Tap the line of editor at [index]
|
||||
Future<void> tapLineOfEditorAt(int index) async {
|
||||
final textBlocks = find.byType(TextBlockComponentWidget);
|
||||
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
|
||||
}
|
||||
|
||||
/// Hover on cover plugin button above the document
|
||||
Future<void> hoverOnCoverPluginAddButton() async {
|
||||
final editor = find.byWidgetPredicate(
|
||||
(widget) => widget is AppFlowyEditor,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
editor,
|
||||
offset: tester.getTopLeft(editor).translate(20, 20),
|
||||
);
|
||||
}
|
||||
|
||||
/// trigger the slash command (selection menu)
|
||||
Future<void> showSlashMenu() async {
|
||||
await tester.ime.insertCharacter('/');
|
||||
}
|
||||
|
||||
/// Tap the slash menu item with [name]
|
||||
///
|
||||
/// Must call [showSlashMenu] first.
|
||||
Future<void> tapSlashMenuItemWithName(String name) async {
|
||||
final slashMenuItem = find.text(name, findRichText: true);
|
||||
await tester.tapButton(slashMenuItem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
const String readme = 'Read me';
|
||||
|
||||
extension Expectation on WidgetTester {
|
||||
/// Expect to see the home page and with a default read me page.
|
||||
void expectToSeeHomePage() {
|
||||
expect(find.byType(HomeStack), findsOneWidget);
|
||||
expect(find.textContaining(readme), findsWidgets);
|
||||
}
|
||||
|
||||
/// Expect to see the page name on the home page.
|
||||
void expectToSeePageName(String name) {
|
||||
final pageName = findPageName(name);
|
||||
expect(pageName, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect not to see the page name on the home page.
|
||||
void expectNotToSeePageName(String name) {
|
||||
final pageName = findPageName(name);
|
||||
expect(pageName, findsNothing);
|
||||
}
|
||||
|
||||
/// Expect to see the document banner.
|
||||
void expectToSeeDocumentBanner() {
|
||||
expect(find.byType(DocumentBanner), findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect not to see the document banner.
|
||||
void expectNotToSeeDocumentBanner() {
|
||||
expect(find.byType(DocumentBanner), findsNothing);
|
||||
}
|
||||
|
||||
/// Expect to the markdown file export success dialog.
|
||||
void expectToExportSuccess() {
|
||||
final exportSuccess = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
expect(exportSuccess, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see the add button and icon button inside the document.
|
||||
void expectToSeePluginAddCoverAndIconButton() {
|
||||
final addCover = find.textContaining(
|
||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||
);
|
||||
final addIcon = find.textContaining(
|
||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||
);
|
||||
expect(addCover, findsOneWidget);
|
||||
expect(addIcon, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see the user name on the home page
|
||||
void expectToSeeUserName(String name) {
|
||||
final userName = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == name,
|
||||
);
|
||||
expect(userName, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see a text
|
||||
void expectToSeeText(String text) {
|
||||
Finder textWidget = find.textContaining(text, findRichText: true);
|
||||
if (textWidget.evaluate().isEmpty) {
|
||||
textWidget = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == text,
|
||||
);
|
||||
}
|
||||
expect(textWidget, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Find the page name on the home page.
|
||||
Finder findPageName(String name) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is ViewSectionItem && widget.view.name == name,
|
||||
skipOffstage: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
extension IME on WidgetTester {
|
||||
IMESimulator get ime => IMESimulator(this);
|
||||
}
|
||||
|
||||
class IMESimulator {
|
||||
IMESimulator(this.tester) {
|
||||
client = findDeltaTextInputClient();
|
||||
}
|
||||
|
||||
final WidgetTester tester;
|
||||
late final DeltaTextInputClient client;
|
||||
|
||||
Future<void> insertText(String text) async {
|
||||
for (final c in text.characters) {
|
||||
await insertCharacter(c);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertCharacter(String character) async {
|
||||
final value = client.currentTextEditingValue;
|
||||
if (value == null) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
final 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,
|
||||
),
|
||||
];
|
||||
client.updateEditingValueWithDeltas(deltas);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
DeltaTextInputClient findDeltaTextInputClient() {
|
||||
final finder = find.byType(KeyboardServiceWidget);
|
||||
final KeyboardServiceWidgetState state = tester.state(finder);
|
||||
return state.textInputService as DeltaTextInputClient;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
const String readme = 'Read me';
|
||||
|
||||
extension AppFlowyLaunch on WidgetTester {
|
||||
Future<void> tapGoButton() async {
|
||||
final goButton = find.byType(GoButton);
|
||||
await tapButton(goButton);
|
||||
}
|
||||
|
||||
Future<void> tapCreateButton() async {
|
||||
await tapButtonWithName(LocaleKeys.settings_files_create.tr());
|
||||
}
|
||||
|
||||
void expectToSeeWelcomePage() {
|
||||
expect(find.byType(HomeStack), findsOneWidget);
|
||||
expect(find.textContaining('Read me'), findsNWidgets(2));
|
||||
}
|
||||
|
||||
Future<void> tapAddButton() async {
|
||||
final addButton = find.byType(AddButton);
|
||||
await tapButton(addButton);
|
||||
}
|
||||
|
||||
Future<void> tapCreateDocumentButton() async {
|
||||
await tapButtonWithName(LocaleKeys.document_menuName.tr());
|
||||
}
|
||||
|
||||
Finder findPageName(String name) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is ViewSectionItem && widget.view.name == name,
|
||||
);
|
||||
}
|
||||
|
||||
void expectToSeePageName(String name) {
|
||||
final pageName = findPageName(name);
|
||||
expect(pageName, findsOneWidget);
|
||||
}
|
||||
|
||||
void expectNotToSeePageName(String name) {
|
||||
final pageName = findPageName(name);
|
||||
expect(pageName, findsNothing);
|
||||
}
|
||||
|
||||
Future<void> hoverOnPageName(String name) async {
|
||||
final pageName = find.byWidgetPredicate(
|
||||
(widget) => widget is ViewSectionItem && widget.view.name == name,
|
||||
);
|
||||
|
||||
final gesture = await createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: Offset.zero);
|
||||
addTearDown(gesture.removePointer);
|
||||
await pump();
|
||||
await gesture.moveTo(getCenter(pageName));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapPageOptionButton() async {
|
||||
final optionButton = find.byType(ViewDisclosureButton);
|
||||
await tapButton(optionButton);
|
||||
}
|
||||
|
||||
Future<void> tapDeletePageButton() async {
|
||||
await tapPageOptionButton();
|
||||
await tapButtonWithName(ViewDisclosureAction.delete.name);
|
||||
}
|
||||
|
||||
void expectToSeeDocumentBanner() {
|
||||
expect(find.byType(DocumentBanner), findsOneWidget);
|
||||
}
|
||||
|
||||
void expectNotToSeeDocumentBanner() {
|
||||
expect(find.byType(DocumentBanner), findsNothing);
|
||||
}
|
||||
|
||||
Future<void> tapRestoreButton() async {
|
||||
final restoreButton = find.textContaining(
|
||||
LocaleKeys.deletePagePrompt_restore.tr(),
|
||||
);
|
||||
await tapButton(restoreButton);
|
||||
}
|
||||
|
||||
Future<void> tapDeletePermanentlyButton() async {
|
||||
final restoreButton = find.textContaining(
|
||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
||||
);
|
||||
await tapButton(restoreButton);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,59 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_impl.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import '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';
|
||||
|
||||
class MockFilePicker extends FilePicker {
|
||||
class MockFilePicker implements FilePickerService {
|
||||
MockFilePicker({
|
||||
required this.mockPath,
|
||||
this.mockPath = '',
|
||||
this.mockPaths = const [],
|
||||
});
|
||||
|
||||
final String mockPath;
|
||||
final List<String> mockPaths;
|
||||
|
||||
@override
|
||||
Future<String?> getDirectoryPath({String? title}) {
|
||||
return Future.value(mockPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> saveFile({
|
||||
String? dialogTitle,
|
||||
String? fileName,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
return Future.value(mockPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FilePickerResult?> pickFiles({
|
||||
String? dialogTitle,
|
||||
String? initialDirectory,
|
||||
fp.FileType type = fp.FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
Function(fp.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();
|
||||
return Future.value(
|
||||
FilePickerResult(
|
||||
platformFiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mockGetDirectoryPath(String? name) async {
|
||||
@@ -27,3 +66,36 @@ Future<void> mockGetDirectoryPath(String? name) async {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<String> mockSaveFilePath(String? name, String fileName) async {
|
||||
final dir = await TestFolder.testLocation(name);
|
||||
final path = p.join(dir.path, fileName);
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPath: path,
|
||||
),
|
||||
);
|
||||
return path;
|
||||
}
|
||||
|
||||
Future<List<String>> mockPickFilePaths(
|
||||
List<String> fileNames, {
|
||||
String? name,
|
||||
String? customPath,
|
||||
}) 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();
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPaths: paths,
|
||||
),
|
||||
);
|
||||
return paths;
|
||||
}
|
||||
@@ -1,33 +1,13 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
enum SettingsPage {
|
||||
appearance,
|
||||
language,
|
||||
files,
|
||||
user,
|
||||
}
|
||||
|
||||
extension on SettingsPage {
|
||||
String get name {
|
||||
switch (this) {
|
||||
case SettingsPage.appearance:
|
||||
return LocaleKeys.settings_menu_appearance.tr();
|
||||
case SettingsPage.language:
|
||||
return LocaleKeys.settings_menu_language.tr();
|
||||
case SettingsPage.files:
|
||||
return LocaleKeys.settings_menu_files.tr();
|
||||
case SettingsPage.user:
|
||||
return LocaleKeys.settings_menu_user.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppFlowySettings on WidgetTester {
|
||||
/// Open settings page
|
||||
Future<void> openSettings() async {
|
||||
@@ -41,7 +21,9 @@ extension AppFlowySettings on WidgetTester {
|
||||
|
||||
/// Open the page that insides the settings page
|
||||
Future<void> openSettingsPage(SettingsPage page) async {
|
||||
final button = find.text(page.name, findRichText: true);
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is SettingsMenuElement && widget.page == page,
|
||||
);
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
return;
|
||||
@@ -50,7 +32,7 @@ extension AppFlowySettings on WidgetTester {
|
||||
/// Restore the AppFlowy data storage location
|
||||
Future<void> restoreLocation() async {
|
||||
final button =
|
||||
find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr());
|
||||
find.byTooltip(LocaleKeys.settings_files_recoverLocationTooltips.tr());
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
return;
|
||||
@@ -64,8 +46,9 @@ extension AppFlowySettings on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> tapCustomLocationButton() async {
|
||||
final button =
|
||||
find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr());
|
||||
final button = find.byTooltip(
|
||||
LocaleKeys.settings_files_changeLocationTooltips.tr(),
|
||||
);
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export 'base.dart';
|
||||
export 'launch.dart';
|
||||
export 'common_operations.dart';
|
||||
export 'settings.dart';
|
||||
export 'data.dart';
|
||||
export 'expectation.dart';
|
||||
export 'editor_test_operations.dart';
|
||||
@@ -6,5 +6,8 @@ import 'startup/startup.dart';
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await FlowyRunner.run(FlowyApp());
|
||||
await FlowyRunner.run(
|
||||
FlowyApp(),
|
||||
integrationEnv(),
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
|
||||
|
||||
@override
|
||||
Widget buildWidget(PluginContext context) => const BlankPage();
|
||||
Widget buildWidget({PluginContext? context}) => const BlankPage();
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
|
||||
+44
-25
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
@@ -22,39 +23,45 @@ import 'cell_service.dart';
|
||||
///
|
||||
// ignore: must_be_immutable
|
||||
class CellController<T, D> extends Equatable {
|
||||
final DatabaseCellContext cellContext;
|
||||
DatabaseCellContext _cellContext;
|
||||
final CellCache _cellCache;
|
||||
final CellCacheKey _cacheKey;
|
||||
final FieldBackendService _fieldBackendSvc;
|
||||
final SingleFieldListener _fieldListener;
|
||||
final CellDataLoader<T> _cellDataLoader;
|
||||
final CellDataPersistence<D> _cellDataPersistence;
|
||||
|
||||
CellListener? _cellListener;
|
||||
RowMetaListener? _rowMetaListener;
|
||||
SingleFieldListener? _fieldListener;
|
||||
CellDataNotifier<T?>? _cellDataNotifier;
|
||||
|
||||
VoidCallback? _onCellFieldChanged;
|
||||
VoidCallback? _onRowMetaChanged;
|
||||
Timer? _loadDataOperation;
|
||||
Timer? _saveDataOperation;
|
||||
|
||||
String get viewId => cellContext.viewId;
|
||||
String get viewId => _cellContext.viewId;
|
||||
|
||||
RowId get rowId => cellContext.rowId;
|
||||
RowId get rowId => _cellContext.rowId;
|
||||
|
||||
String get fieldId => cellContext.fieldInfo.id;
|
||||
String get fieldId => _cellContext.fieldInfo.id;
|
||||
|
||||
FieldInfo get fieldInfo => cellContext.fieldInfo;
|
||||
FieldInfo get fieldInfo => _cellContext.fieldInfo;
|
||||
|
||||
FieldType get fieldType => cellContext.fieldInfo.fieldType;
|
||||
FieldType get fieldType => _cellContext.fieldInfo.fieldType;
|
||||
|
||||
String? get emoji => _cellContext.emoji;
|
||||
|
||||
CellController({
|
||||
required this.cellContext,
|
||||
required DatabaseCellContext cellContext,
|
||||
required CellCache cellCache,
|
||||
required CellDataLoader<T> cellDataLoader,
|
||||
required CellDataPersistence<D> cellDataPersistence,
|
||||
}) : _cellCache = cellCache,
|
||||
}) : _cellContext = cellContext,
|
||||
_cellCache = cellCache,
|
||||
_cellDataLoader = cellDataLoader,
|
||||
_cellDataPersistence = cellDataPersistence,
|
||||
_rowMetaListener = RowMetaListener(cellContext.rowId),
|
||||
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
|
||||
_fieldBackendSvc = FieldBackendService(
|
||||
viewId: cellContext.viewId,
|
||||
@@ -84,20 +91,22 @@ class CellController<T, D> extends Equatable {
|
||||
);
|
||||
|
||||
/// 2.Listen on the field event and load the cell data if needed.
|
||||
_fieldListener.start(
|
||||
onFieldChanged: (result) {
|
||||
result.fold(
|
||||
(fieldPB) {
|
||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
||||
/// For example:
|
||||
/// ¥12 -> $12
|
||||
if (_cellDataLoader.reloadOnFieldChanged) {
|
||||
_loadData();
|
||||
}
|
||||
_onCellFieldChanged?.call();
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
_fieldListener?.start(
|
||||
onFieldChanged: (fieldPB) {
|
||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
||||
/// For example:
|
||||
/// ¥12 -> $12
|
||||
if (_cellDataLoader.reloadOnFieldChanged) {
|
||||
_loadData();
|
||||
}
|
||||
_onCellFieldChanged?.call();
|
||||
},
|
||||
);
|
||||
|
||||
_rowMetaListener?.start(
|
||||
callback: (newRowMeta) {
|
||||
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
|
||||
_onRowMetaChanged?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -105,9 +114,11 @@ class CellController<T, D> extends Equatable {
|
||||
/// Listen on the cell content or field changes
|
||||
VoidCallback? startListening({
|
||||
required void Function(T?) onCellChanged,
|
||||
VoidCallback? onRowMetaChanged,
|
||||
VoidCallback? onCellFieldChanged,
|
||||
}) {
|
||||
_onCellFieldChanged = onCellFieldChanged;
|
||||
_onRowMetaChanged = onRowMetaChanged;
|
||||
|
||||
/// Notify the listener, the cell data was changed.
|
||||
onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
|
||||
@@ -186,18 +197,26 @@ class CellController<T, D> extends Equatable {
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _rowMetaListener?.stop();
|
||||
_rowMetaListener = null;
|
||||
|
||||
await _cellListener?.stop();
|
||||
_cellListener = null;
|
||||
|
||||
await _fieldListener?.stop();
|
||||
_fieldListener = null;
|
||||
|
||||
_loadDataOperation?.cancel();
|
||||
_saveDataOperation?.cancel();
|
||||
_cellDataNotifier?.dispose();
|
||||
await _fieldListener.stop();
|
||||
_cellDataNotifier = null;
|
||||
_onRowMetaChanged = null;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
_cellCache.get(_cacheKey) ?? "",
|
||||
cellContext.rowId + cellContext.fieldInfo.id
|
||||
_cellContext.rowId + _cellContext.fieldInfo.id
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
|
||||
@override
|
||||
Future<Option<FlowyError>> save(DateCellData data) {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellPath = _makeCellPath(cellContext);
|
||||
..cellId = _makeCellPath(cellContext);
|
||||
if (data.dateTime != null) {
|
||||
final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
payload.date = date;
|
||||
|
||||
+8
-2
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@@ -52,18 +53,23 @@ class CellBackendService {
|
||||
class DatabaseCellContext with _$DatabaseCellContext {
|
||||
const factory DatabaseCellContext({
|
||||
required String viewId,
|
||||
required RowId rowId,
|
||||
required RowMetaPB rowMeta,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _DatabaseCellContext;
|
||||
|
||||
// ignore: unused_element
|
||||
const DatabaseCellContext._();
|
||||
|
||||
String get rowId => rowMeta.id;
|
||||
|
||||
String get fieldId => fieldInfo.id;
|
||||
|
||||
FieldType get fieldType => fieldInfo.fieldType;
|
||||
|
||||
ValueKey key() {
|
||||
return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
|
||||
return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}");
|
||||
}
|
||||
|
||||
/// Only the primary field can have an emoji.
|
||||
String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null;
|
||||
}
|
||||
+23
-19
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||
@@ -7,17 +6,23 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class ChecklistCellBackendService {
|
||||
final DatabaseCellContext cellContext;
|
||||
final String viewId;
|
||||
final String fieldId;
|
||||
final String rowId;
|
||||
|
||||
ChecklistCellBackendService({required this.cellContext});
|
||||
ChecklistCellBackendService({
|
||||
required this.viewId,
|
||||
required this.fieldId,
|
||||
required this.rowId,
|
||||
});
|
||||
|
||||
Future<Either<Unit, FlowyError>> create({
|
||||
required String name,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..insertOptions.add(name);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@@ -27,9 +32,9 @@ class ChecklistCellBackendService {
|
||||
required List<String> optionIds,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..deleteOptionIds.addAll(optionIds);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@@ -39,9 +44,9 @@ class ChecklistCellBackendService {
|
||||
required String optionId,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..selectedOptionIds.add(optionId);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@@ -51,9 +56,9 @@ class ChecklistCellBackendService {
|
||||
required SelectOptionPB option,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = cellContext.viewId
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..rowId = cellContext.rowId
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..updateOptions.add(option);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
@@ -61,10 +66,9 @@ class ChecklistCellBackendService {
|
||||
|
||||
Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
|
||||
final payload = CellIdPB.create()
|
||||
..fieldId = cellContext.fieldInfo.id
|
||||
..viewId = cellContext.viewId
|
||||
..rowId = cellContext.rowId
|
||||
..rowId = cellContext.rowId;
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
|
||||
return DatabaseEventGetChecklistCellData(payload).send();
|
||||
}
|
||||
|
||||
+8
-7
@@ -1,6 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
@@ -8,12 +6,15 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||
|
||||
class SelectOptionCellBackendService {
|
||||
final DatabaseCellContext cellContext;
|
||||
SelectOptionCellBackendService({required this.cellContext});
|
||||
final String viewId;
|
||||
final String fieldId;
|
||||
final String rowId;
|
||||
|
||||
String get viewId => cellContext.viewId;
|
||||
String get fieldId => cellContext.fieldInfo.id;
|
||||
RowId get rowId => cellContext.rowId;
|
||||
SelectOptionCellBackendService({
|
||||
required this.viewId,
|
||||
required this.fieldId,
|
||||
required this.rowId,
|
||||
});
|
||||
|
||||
Future<Either<Unit, FlowyError>> create({
|
||||
required String name,
|
||||
|
||||
+126
-97
@@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart';
|
||||
@@ -15,6 +15,7 @@ import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'database_view_service.dart';
|
||||
import 'defines.dart';
|
||||
import 'layout/layout_service.dart';
|
||||
import 'layout/layout_setting_listener.dart';
|
||||
import 'row/row_cache.dart';
|
||||
import 'group/group_listener.dart';
|
||||
@@ -49,16 +50,11 @@ class DatabaseLayoutSettingCallbacks {
|
||||
});
|
||||
}
|
||||
|
||||
class CalendarLayoutCallbacks {
|
||||
final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged;
|
||||
|
||||
CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
|
||||
}
|
||||
|
||||
class DatabaseCallbacks {
|
||||
OnDatabaseChanged? onDatabaseChanged;
|
||||
OnFieldsChanged? onFieldsChanged;
|
||||
OnFiltersChanged? onFiltersChanged;
|
||||
OnSortsChanged? onSortsChanged;
|
||||
OnNumOfRowsChanged? onNumOfRowsChanged;
|
||||
OnRowsDeleted? onRowsDeleted;
|
||||
OnRowsUpdated? onRowsUpdated;
|
||||
@@ -69,6 +65,7 @@ class DatabaseCallbacks {
|
||||
this.onNumOfRowsChanged,
|
||||
this.onFieldsChanged,
|
||||
this.onFiltersChanged,
|
||||
this.onSortsChanged,
|
||||
this.onRowsUpdated,
|
||||
this.onRowsDeleted,
|
||||
this.onRowsCreated,
|
||||
@@ -79,30 +76,29 @@ class DatabaseController {
|
||||
final String viewId;
|
||||
final DatabaseViewBackendService _databaseViewBackendSvc;
|
||||
final FieldController fieldController;
|
||||
DatabaseLayoutPB? databaseLayout;
|
||||
DatabaseLayoutPB databaseLayout;
|
||||
DatabaseLayoutSettingPB? databaseLayoutSetting;
|
||||
late DatabaseViewCache _viewCache;
|
||||
|
||||
// Callbacks
|
||||
DatabaseCallbacks? _databaseCallbacks;
|
||||
GroupCallbacks? _groupCallbacks;
|
||||
DatabaseLayoutSettingCallbacks? _layoutCallbacks;
|
||||
CalendarLayoutCallbacks? _calendarLayoutCallbacks;
|
||||
final List<DatabaseCallbacks> _databaseCallbacks = [];
|
||||
final List<GroupCallbacks> _groupCallbacks = [];
|
||||
final List<DatabaseLayoutSettingCallbacks> _layoutCallbacks = [];
|
||||
|
||||
// Getters
|
||||
RowCache get rowCache => _viewCache.rowCache;
|
||||
|
||||
// Listener
|
||||
final DatabaseGroupListener groupListener;
|
||||
final DatabaseLayoutSettingListener layoutListener;
|
||||
final DatabaseCalendarLayoutListener calendarLayoutListener;
|
||||
final DatabaseGroupListener _groupListener;
|
||||
final DatabaseLayoutSettingListener _layoutListener;
|
||||
|
||||
DatabaseController({required ViewPB view})
|
||||
: viewId = view.id,
|
||||
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
|
||||
fieldController = FieldController(viewId: view.id),
|
||||
groupListener = DatabaseGroupListener(view.id),
|
||||
layoutListener = DatabaseLayoutSettingListener(view.id),
|
||||
calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
|
||||
_groupListener = DatabaseGroupListener(view.id),
|
||||
databaseLayout = databaseLayoutFromViewLayout(view.layout),
|
||||
_layoutListener = DatabaseLayoutSettingListener(view.id) {
|
||||
_viewCache = DatabaseViewCache(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
@@ -113,44 +109,51 @@ class DatabaseController {
|
||||
_listenOnLayoutChanged();
|
||||
}
|
||||
|
||||
void setListener({
|
||||
void addListener({
|
||||
DatabaseCallbacks? onDatabaseChanged,
|
||||
DatabaseLayoutSettingCallbacks? onLayoutChanged,
|
||||
GroupCallbacks? onGroupChanged,
|
||||
CalendarLayoutCallbacks? onCalendarLayoutChanged,
|
||||
}) {
|
||||
_layoutCallbacks = onLayoutChanged;
|
||||
_databaseCallbacks = onDatabaseChanged;
|
||||
_groupCallbacks = onGroupChanged;
|
||||
_calendarLayoutCallbacks = onCalendarLayoutChanged;
|
||||
if (onLayoutChanged != null) {
|
||||
_layoutCallbacks.add(onLayoutChanged);
|
||||
}
|
||||
|
||||
if (onDatabaseChanged != null) {
|
||||
_databaseCallbacks.add(onDatabaseChanged);
|
||||
}
|
||||
|
||||
if (onGroupChanged != null) {
|
||||
_groupCallbacks.add(onGroupChanged);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> open() async {
|
||||
return _databaseViewBackendSvc.openGrid().then((result) {
|
||||
return _databaseViewBackendSvc.openDatabase().then((result) {
|
||||
return result.fold(
|
||||
(database) async {
|
||||
(DatabasePB database) async {
|
||||
databaseLayout = database.layoutType;
|
||||
|
||||
if (databaseLayout == DatabaseLayoutPB.Calendar) {
|
||||
_listenOnCalendarLayoutChanged();
|
||||
}
|
||||
|
||||
_databaseCallbacks?.onDatabaseChanged?.call(database);
|
||||
_viewCache.rowCache.setInitialRows(database.rows);
|
||||
return await fieldController
|
||||
.loadFields(
|
||||
// Load the actual database field data.
|
||||
final fieldsOrFail = await fieldController.loadFields(
|
||||
fieldIds: database.fields,
|
||||
)
|
||||
.then(
|
||||
(result) {
|
||||
return result.fold(
|
||||
(l) => Future(() async {
|
||||
await _loadGroups();
|
||||
await _loadLayoutSetting();
|
||||
return left(l);
|
||||
}),
|
||||
(err) => right(err),
|
||||
);
|
||||
);
|
||||
return fieldsOrFail.fold(
|
||||
(fields) {
|
||||
// Notify the database is changed after the fields are loaded.
|
||||
// The database won't can't be used until the fields are loaded.
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onDatabaseChanged?.call(database);
|
||||
}
|
||||
_viewCache.rowCache.setInitialRows(database.rows);
|
||||
return Future(() async {
|
||||
await _loadGroups();
|
||||
await _loadLayoutSetting();
|
||||
return left(fields);
|
||||
});
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return right(err);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -159,7 +162,7 @@ class DatabaseController {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<RowPB, FlowyError>> createRow({
|
||||
Future<Either<RowMetaPB, FlowyError>> createRow({
|
||||
RowId? startRowId,
|
||||
String? groupId,
|
||||
void Function(RowDataBuilder builder)? withCells,
|
||||
@@ -180,9 +183,9 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> moveGroupRow({
|
||||
required RowPB fromRow,
|
||||
required RowMetaPB fromRow,
|
||||
required String groupId,
|
||||
RowPB? toRow,
|
||||
RowMetaPB? toRow,
|
||||
}) {
|
||||
return _databaseViewBackendSvc.moveGroupRow(
|
||||
fromRowId: fromRow.id,
|
||||
@@ -192,12 +195,12 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> moveRow({
|
||||
required RowPB fromRow,
|
||||
required RowPB toRow,
|
||||
required String fromRowId,
|
||||
required String toRowId,
|
||||
}) {
|
||||
return _databaseViewBackendSvc.moveRow(
|
||||
fromRowId: fromRow.id,
|
||||
toRowId: toRow.id,
|
||||
fromRowId: fromRowId,
|
||||
toRowId: toRowId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,11 +214,14 @@ class DatabaseController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateCalenderLayoutSetting(
|
||||
CalendarLayoutSettingPB layoutSetting,
|
||||
Future<void> updateLayoutSetting(
|
||||
CalendarLayoutSettingPB calendarlLayoutSetting,
|
||||
) async {
|
||||
await _databaseViewBackendSvc
|
||||
.updateLayoutSetting(calendarLayoutSetting: layoutSetting)
|
||||
.updateLayoutSetting(
|
||||
calendarLayoutSetting: calendarlLayoutSetting,
|
||||
layoutType: databaseLayout,
|
||||
)
|
||||
.then((result) {
|
||||
result.fold((l) => null, (r) => Log.error(r));
|
||||
});
|
||||
@@ -224,12 +230,11 @@ class DatabaseController {
|
||||
Future<void> dispose() async {
|
||||
await _databaseViewBackendSvc.closeView();
|
||||
await fieldController.dispose();
|
||||
await groupListener.stop();
|
||||
await _groupListener.stop();
|
||||
await _viewCache.dispose();
|
||||
_databaseCallbacks = null;
|
||||
_groupCallbacks = null;
|
||||
_layoutCallbacks = null;
|
||||
_calendarLayoutCallbacks = null;
|
||||
_databaseCallbacks.clear();
|
||||
_groupCallbacks.clear();
|
||||
_layoutCallbacks.clear();
|
||||
}
|
||||
|
||||
Future<void> _loadGroups() async {
|
||||
@@ -237,7 +242,9 @@ class DatabaseController {
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(groups) {
|
||||
_groupCallbacks?.onGroupByField?.call(groups.items);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onGroupByField?.call(groups.items);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
),
|
||||
@@ -245,60 +252,88 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
Future<void> _loadLayoutSetting() async {
|
||||
if (databaseLayout != null) {
|
||||
_databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) {
|
||||
result.fold(
|
||||
(l) => _layoutCallbacks?.onLoadLayout(l),
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
_databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) {
|
||||
result.fold(
|
||||
(newDatabaseLayoutSetting) {
|
||||
databaseLayoutSetting = newDatabaseLayoutSetting;
|
||||
databaseLayoutSetting?.freeze();
|
||||
|
||||
for (final callback in _layoutCallbacks) {
|
||||
callback.onLoadLayout(newDatabaseLayoutSetting);
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _listenOnRowsChanged() {
|
||||
final callbacks = DatabaseViewCallbacks(
|
||||
onNumOfRowsChanged: (rows, rowByRowId, reason) {
|
||||
_databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
|
||||
}
|
||||
},
|
||||
onRowsDeleted: (ids) {
|
||||
_databaseCallbacks?.onRowsDeleted?.call(ids);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onRowsDeleted?.call(ids);
|
||||
}
|
||||
},
|
||||
onRowsUpdated: (ids) {
|
||||
_databaseCallbacks?.onRowsUpdated?.call(ids);
|
||||
onRowsUpdated: (ids, reason) {
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onRowsUpdated?.call(ids, reason);
|
||||
}
|
||||
},
|
||||
onRowsCreated: (ids) {
|
||||
_databaseCallbacks?.onRowsCreated?.call(ids);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onRowsCreated?.call(ids);
|
||||
}
|
||||
},
|
||||
);
|
||||
_viewCache.setListener(callbacks);
|
||||
_viewCache.addListener(callbacks);
|
||||
}
|
||||
|
||||
void _listenOnFieldsChanged() {
|
||||
fieldController.addListener(
|
||||
onReceiveFields: (fields) {
|
||||
_databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields));
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onFieldsChanged?.call(UnmodifiableListView(fields));
|
||||
}
|
||||
},
|
||||
onSorts: (sorts) {
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onSortsChanged?.call(sorts);
|
||||
}
|
||||
},
|
||||
onFilters: (filters) {
|
||||
_databaseCallbacks?.onFiltersChanged?.call(filters);
|
||||
for (final callback in _databaseCallbacks) {
|
||||
callback.onFiltersChanged?.call(filters);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _listenOnGroupChanged() {
|
||||
groupListener.start(
|
||||
_groupListener.start(
|
||||
onNumOfGroupsChanged: (result) {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
if (changeset.updateGroups.isNotEmpty) {
|
||||
_groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onUpdateGroup?.call(changeset.updateGroups);
|
||||
}
|
||||
}
|
||||
|
||||
if (changeset.deletedGroups.isNotEmpty) {
|
||||
_groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onDeleteGroup?.call(changeset.deletedGroups);
|
||||
}
|
||||
}
|
||||
|
||||
for (final insertedGroup in changeset.insertedGroups) {
|
||||
_groupCallbacks?.onInsertGroup?.call(insertedGroup);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onInsertGroup?.call(insertedGroup);
|
||||
}
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
@@ -307,7 +342,9 @@ class DatabaseController {
|
||||
onGroupByNewField: (result) {
|
||||
result.fold(
|
||||
(groups) {
|
||||
_groupCallbacks?.onGroupByField?.call(groups);
|
||||
for (final callback in _groupCallbacks) {
|
||||
callback.onGroupByField?.call(groups);
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
@@ -316,24 +353,16 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
void _listenOnLayoutChanged() {
|
||||
layoutListener.start(
|
||||
_layoutListener.start(
|
||||
onLayoutChanged: (result) {
|
||||
result.fold(
|
||||
(l) {
|
||||
_layoutCallbacks?.onLayoutChanged(l);
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
(newLayout) {
|
||||
databaseLayoutSetting = newLayout;
|
||||
databaseLayoutSetting?.freeze();
|
||||
|
||||
void _listenOnCalendarLayoutChanged() {
|
||||
calendarLayoutListener.start(
|
||||
onCalendarLayoutChanged: (result) {
|
||||
result.fold(
|
||||
(l) {
|
||||
_calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
|
||||
for (final callback in _layoutCallbacks) {
|
||||
callback.onLayoutChanged(newLayout);
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
|
||||
+6
-3
@@ -25,12 +25,12 @@ class DatabaseViewBackendService {
|
||||
.then((value) => value.leftMap((l) => l.value));
|
||||
}
|
||||
|
||||
Future<Either<DatabasePB, FlowyError>> openGrid() async {
|
||||
Future<Either<DatabasePB, FlowyError>> openDatabase() async {
|
||||
final payload = DatabaseViewIdPB(value: viewId);
|
||||
return DatabaseEventGetDatabase(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<RowPB, FlowyError>> createRow({
|
||||
Future<Either<RowMetaPB, FlowyError>> createRow({
|
||||
RowId? startRowId,
|
||||
String? groupId,
|
||||
Map<String, String>? cellDataByFieldId,
|
||||
@@ -113,9 +113,12 @@ class DatabaseViewBackendService {
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateLayoutSetting({
|
||||
required DatabaseLayoutPB layoutType,
|
||||
CalendarLayoutSettingPB? calendarLayoutSetting,
|
||||
}) {
|
||||
final payload = LayoutSettingChangesetPB.create()..viewId = viewId;
|
||||
final payload = LayoutSettingChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..layoutType = layoutType;
|
||||
if (calendarLayoutSetting != null) {
|
||||
payload.calendar = calendarLayoutSetting;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
|
||||
@@ -10,10 +11,14 @@ import 'row/row_service.dart';
|
||||
|
||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
||||
typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
||||
typedef OnSortsChanged = void Function(List<SortInfo>);
|
||||
typedef OnDatabaseChanged = void Function(DatabasePB);
|
||||
|
||||
typedef OnRowsCreated = void Function(List<RowId> ids);
|
||||
typedef OnRowsUpdated = void Function(List<RowId> ids);
|
||||
typedef OnRowsUpdated = void Function(
|
||||
List<RowId> ids,
|
||||
RowsChangedReason reason,
|
||||
);
|
||||
typedef OnRowsDeleted = void Function(List<RowId> ids);
|
||||
typedef OnNumOfRowsChanged = void Function(
|
||||
UnmodifiableListView<RowInfo> rows,
|
||||
|
||||
+2
-6
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -52,14 +51,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
|
||||
void _startListening() {
|
||||
_fieldListener.start(
|
||||
onFieldChanged: (result) {
|
||||
onFieldChanged: (updatedField) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
result.fold(
|
||||
(field) => add(FieldCellEvent.didReceiveFieldUpdate(field)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
add(FieldCellEvent.didReceiveFieldUpdate(updatedField));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+6
-8
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
|
||||
@@ -7,12 +8,11 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
|
||||
typedef UpdateFieldNotifiedValue = Either<FieldPB, FlowyError>;
|
||||
typedef UpdateFieldNotifiedValue = FieldPB;
|
||||
|
||||
class SingleFieldListener {
|
||||
final String fieldId;
|
||||
PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier =
|
||||
PublishNotifier();
|
||||
void Function(UpdateFieldNotifiedValue)? _updateFieldNotifier;
|
||||
DatabaseNotificationListener? _listener;
|
||||
|
||||
SingleFieldListener({required this.fieldId});
|
||||
@@ -20,7 +20,7 @@ class SingleFieldListener {
|
||||
void start({
|
||||
required void Function(UpdateFieldNotifiedValue) onFieldChanged,
|
||||
}) {
|
||||
_updateFieldNotifier?.addPublishListener(onFieldChanged);
|
||||
_updateFieldNotifier = onFieldChanged;
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: fieldId,
|
||||
handler: _handler,
|
||||
@@ -34,9 +34,8 @@ class SingleFieldListener {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateField:
|
||||
result.fold(
|
||||
(payload) =>
|
||||
_updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
|
||||
(error) => _updateFieldNotifier?.value = right(error),
|
||||
(payload) => _updateFieldNotifier?.call(FieldPB.fromBuffer(payload)),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -46,7 +45,6 @@ class SingleFieldListener {
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_updateFieldNotifier?.dispose();
|
||||
_updateFieldNotifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -99,6 +99,14 @@ class FieldBackendService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the primary field of the view.
|
||||
static Future<Either<FieldPB, FlowyError>> getPrimaryField({
|
||||
required String viewId,
|
||||
}) {
|
||||
final payload = DatabaseViewIdPB.create()..value = viewId;
|
||||
return DatabaseEventGetPrimaryField(payload).send();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+1
-4
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@@ -10,12 +9,10 @@ class GroupBackendService {
|
||||
|
||||
Future<Either<Unit, FlowyError>> groupByField({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
final payload = GroupByFieldPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType;
|
||||
..fieldId = fieldId;
|
||||
|
||||
return DatabaseEventSetGroupByField(payload).send();
|
||||
}
|
||||
|
||||
+15
-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;
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'row_meta_listener.dart';
|
||||
|
||||
part 'row_banner_bloc.freezed.dart';
|
||||
|
||||
class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
||||
final String viewId;
|
||||
final RowBackendService _rowBackendSvc;
|
||||
final RowMetaListener _metaListener;
|
||||
SingleFieldListener? _fieldListener;
|
||||
|
||||
RowBannerBloc({
|
||||
required this.viewId,
|
||||
required RowMetaPB rowMeta,
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||
_metaListener = RowMetaListener(rowMeta.id),
|
||||
super(RowBannerState.initial(rowMeta)) {
|
||||
on<RowBannerEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () async {
|
||||
_loadPrimaryField();
|
||||
await _listenRowMeteChanged();
|
||||
},
|
||||
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
rowMeta: rowMeta,
|
||||
),
|
||||
);
|
||||
},
|
||||
setCover: (String coverURL) {
|
||||
_updateMeta(coverURL: coverURL);
|
||||
},
|
||||
setIcon: (String iconURL) {
|
||||
_updateMeta(iconURL: iconURL);
|
||||
},
|
||||
didReceiveFieldUpdate: (updatedField) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
primaryField: updatedField,
|
||||
loadingState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _metaListener.stop();
|
||||
await _fieldListener?.stop();
|
||||
_fieldListener = null;
|
||||
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _loadPrimaryField() async {
|
||||
final fieldOrError =
|
||||
await FieldBackendService.getPrimaryField(viewId: viewId);
|
||||
fieldOrError.fold(
|
||||
(primaryField) {
|
||||
if (!isClosed) {
|
||||
_fieldListener = SingleFieldListener(fieldId: primaryField.id);
|
||||
_fieldListener?.start(
|
||||
onFieldChanged: (updatedField) {
|
||||
if (!isClosed) {
|
||||
add(RowBannerEvent.didReceiveFieldUpdate(updatedField));
|
||||
}
|
||||
},
|
||||
);
|
||||
add(RowBannerEvent.didReceiveFieldUpdate(primaryField));
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen the changes of the row meta and then update the banner
|
||||
Future<void> _listenRowMeteChanged() async {
|
||||
_metaListener.start(
|
||||
callback: (rowMeta) {
|
||||
add(RowBannerEvent.didReceiveRowMeta(rowMeta));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the meta of the row and the view
|
||||
Future<void> _updateMeta({
|
||||
String? iconURL,
|
||||
String? coverURL,
|
||||
}) async {
|
||||
// Most of the time, the result is success, so we don't need to handle it.
|
||||
await _rowBackendSvc
|
||||
.updateMeta(
|
||||
iconURL: iconURL,
|
||||
coverURL: coverURL,
|
||||
rowId: state.rowMeta.id,
|
||||
)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
|
||||
// Set the icon and cover of the view
|
||||
ViewBackendService.updateView(
|
||||
viewId: viewId,
|
||||
iconURL: iconURL,
|
||||
coverURL: coverURL,
|
||||
).then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowBannerEvent with _$RowBannerEvent {
|
||||
const factory RowBannerEvent.initial() = _Initial;
|
||||
const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
|
||||
_DidReceiveRowMeta;
|
||||
const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
|
||||
_DidReceiveFieldUdate;
|
||||
const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
|
||||
const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowBannerState with _$RowBannerState {
|
||||
const factory RowBannerState({
|
||||
ViewPB? view,
|
||||
FieldPB? primaryField,
|
||||
required RowMetaPB rowMeta,
|
||||
required LoadingState loadingState,
|
||||
}) = _RowBannerState;
|
||||
|
||||
factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState(
|
||||
rowMeta: rowMetaPB,
|
||||
loadingState: const LoadingState.loading(),
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.loading() = _Loading;
|
||||
const factory LoadingState.finish() = _Finish;
|
||||
}
|
||||
+51
-48
@@ -38,17 +38,21 @@ class RowCache {
|
||||
final RowCacheDelegate _delegate;
|
||||
final RowChangesetNotifier _rowChangeReasonNotifier;
|
||||
|
||||
/// Returns a unmodifiable list of RowInfo
|
||||
UnmodifiableListView<RowInfo> get rowInfos {
|
||||
final visibleRows = [..._rowList.rows];
|
||||
return UnmodifiableListView(visibleRows);
|
||||
}
|
||||
|
||||
/// Returns a unmodifiable map of rowId to RowInfo
|
||||
UnmodifiableMapView<RowId, RowInfo> get rowByRowId {
|
||||
return UnmodifiableMapView(_rowList.rowInfoByRowId);
|
||||
}
|
||||
|
||||
CellCache get cellCache => _cellCache;
|
||||
|
||||
RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason;
|
||||
|
||||
RowCache({
|
||||
required this.viewId,
|
||||
required RowFieldsDelegate fieldsDelegate,
|
||||
@@ -70,7 +74,7 @@ class RowCache {
|
||||
return _rowList.get(rowId);
|
||||
}
|
||||
|
||||
void setInitialRows(List<RowPB> rows) {
|
||||
void setInitialRows(List<RowMetaPB> rows) {
|
||||
for (final row in rows) {
|
||||
final rowInfo = buildGridRow(row);
|
||||
_rowList.add(rowInfo);
|
||||
@@ -128,7 +132,7 @@ class RowCache {
|
||||
void _insertRows(List<InsertedRowPB> insertRows) {
|
||||
for (final insertedRow in insertRows) {
|
||||
final insertedIndex =
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
|
||||
if (insertedIndex != null) {
|
||||
_rowChangeReasonNotifier
|
||||
.receive(RowsChangedReason.insert(insertedIndex));
|
||||
@@ -138,20 +142,23 @@ class RowCache {
|
||||
|
||||
void _updateRows(List<UpdatedRowPB> updatedRows) {
|
||||
if (updatedRows.isEmpty) return;
|
||||
final List<RowPB> rowPBs = [];
|
||||
final List<RowMetaPB> updatedList = [];
|
||||
for (final updatedRow in updatedRows) {
|
||||
for (final fieldId in updatedRow.fieldIds) {
|
||||
final key = CellCacheKey(
|
||||
fieldId: fieldId,
|
||||
rowId: updatedRow.row.id,
|
||||
rowId: updatedRow.rowId,
|
||||
);
|
||||
_cellCache.remove(key);
|
||||
}
|
||||
rowPBs.add(updatedRow.row);
|
||||
if (updatedRow.hasRowMeta()) {
|
||||
updatedList.add(updatedRow.rowMeta);
|
||||
}
|
||||
}
|
||||
|
||||
final updatedIndexs =
|
||||
_rowList.updateRows(rowPBs, (rowPB) => buildGridRow(rowPB));
|
||||
_rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId));
|
||||
|
||||
if (updatedIndexs.isNotEmpty) {
|
||||
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
||||
}
|
||||
@@ -169,7 +176,7 @@ class RowCache {
|
||||
void _showRows(List<InsertedRowPB> visibleRows) {
|
||||
for (final insertedRow in visibleRows) {
|
||||
final insertedIndex =
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
|
||||
if (insertedIndex != null) {
|
||||
_rowChangeReasonNotifier
|
||||
.receive(RowsChangedReason.insert(insertedIndex));
|
||||
@@ -197,8 +204,9 @@ class RowCache {
|
||||
if (onCellUpdated != null) {
|
||||
final rowInfo = _rowList.get(rowId);
|
||||
if (rowInfo != null) {
|
||||
final CellContextByFieldId cellDataMap =
|
||||
_makeGridCells(rowId, rowInfo.rowPB);
|
||||
final CellContextByFieldId cellDataMap = _makeGridCells(
|
||||
rowInfo.rowMeta,
|
||||
);
|
||||
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
|
||||
}
|
||||
}
|
||||
@@ -220,12 +228,12 @@ class RowCache {
|
||||
_rowChangeReasonNotifier.removeListener(callback);
|
||||
}
|
||||
|
||||
CellContextByFieldId loadGridCells(RowId rowId) {
|
||||
final RowPB? data = _rowList.get(rowId)?.rowPB;
|
||||
if (data == null) {
|
||||
_loadRow(rowId);
|
||||
CellContextByFieldId loadGridCells(RowMetaPB rowMeta) {
|
||||
final rowInfo = _rowList.get(rowMeta.id);
|
||||
if (rowInfo == null) {
|
||||
_loadRow(rowMeta.id);
|
||||
}
|
||||
return _makeGridCells(rowId, data);
|
||||
return _makeGridCells(rowMeta);
|
||||
}
|
||||
|
||||
Future<void> _loadRow(RowId rowId) async {
|
||||
@@ -233,57 +241,51 @@ class RowCache {
|
||||
..viewId = viewId
|
||||
..rowId = rowId;
|
||||
|
||||
final result = await DatabaseEventGetRow(payload).send();
|
||||
final result = await DatabaseEventGetRowMeta(payload).send();
|
||||
result.fold(
|
||||
(optionRow) => _refreshRow(optionRow),
|
||||
(rowMetaPB) {
|
||||
final rowInfo = _rowList.get(rowMetaPB.id);
|
||||
final rowIndex = _rowList.indexOfRow(rowMetaPB.id);
|
||||
if (rowInfo != null && rowIndex != null) {
|
||||
final updatedRowInfo = rowInfo.copyWith(rowMeta: rowMetaPB);
|
||||
_rowList.remove(rowMetaPB.id);
|
||||
_rowList.insert(rowIndex, updatedRowInfo);
|
||||
|
||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||
updatedIndexs[rowMetaPB.id] = UpdatedIndex(
|
||||
index: rowIndex,
|
||||
rowId: rowMetaPB.id,
|
||||
);
|
||||
|
||||
_rowChangeReasonNotifier
|
||||
.receive(RowsChangedReason.update(updatedIndexs));
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
CellContextByFieldId _makeGridCells(RowId rowId, RowPB? row) {
|
||||
CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) {
|
||||
// ignore: prefer_collection_literals
|
||||
final cellDataMap = CellContextByFieldId();
|
||||
final cellContextMap = CellContextByFieldId();
|
||||
for (final field in _delegate.fields) {
|
||||
if (field.visibility) {
|
||||
cellDataMap[field.id] = DatabaseCellContext(
|
||||
rowId: rowId,
|
||||
cellContextMap[field.id] = DatabaseCellContext(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
fieldInfo: field,
|
||||
);
|
||||
}
|
||||
}
|
||||
return cellDataMap;
|
||||
return cellContextMap;
|
||||
}
|
||||
|
||||
void _refreshRow(OptionalRowPB optionRow) {
|
||||
if (!optionRow.hasRow()) {
|
||||
return;
|
||||
}
|
||||
final updatedRow = optionRow.row;
|
||||
updatedRow.freeze();
|
||||
|
||||
final rowInfo = _rowList.get(updatedRow.id);
|
||||
final rowIndex = _rowList.indexOfRow(updatedRow.id);
|
||||
if (rowInfo != null && rowIndex != null) {
|
||||
final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow);
|
||||
_rowList.remove(updatedRow.id);
|
||||
_rowList.insert(rowIndex, updatedRowInfo);
|
||||
|
||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||
updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
|
||||
index: rowIndex,
|
||||
rowId: updatedRowInfo.rowPB.id,
|
||||
);
|
||||
|
||||
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
||||
}
|
||||
}
|
||||
|
||||
RowInfo buildGridRow(RowPB rowPB) {
|
||||
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
|
||||
return RowInfo(
|
||||
viewId: viewId,
|
||||
fields: _delegate.fields,
|
||||
rowPB: rowPB,
|
||||
rowId: rowMetaPB.id,
|
||||
rowMeta: rowMetaPB,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -310,9 +312,10 @@ class RowChangesetNotifier extends ChangeNotifier {
|
||||
@unfreezed
|
||||
class RowInfo with _$RowInfo {
|
||||
factory RowInfo({
|
||||
required String rowId,
|
||||
required String viewId,
|
||||
required UnmodifiableListView<FieldInfo> fields,
|
||||
required RowPB rowPB,
|
||||
required RowMetaPB rowMeta,
|
||||
}) = _RowInfo;
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -1,12 +1,12 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../cell/cell_service.dart';
|
||||
import 'row_cache.dart';
|
||||
import 'row_service.dart';
|
||||
|
||||
typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason);
|
||||
|
||||
class RowController {
|
||||
final RowId rowId;
|
||||
final RowMetaPB rowMeta;
|
||||
final String? groupId;
|
||||
final String viewId;
|
||||
final List<VoidCallback> _onRowChangedListeners = [];
|
||||
@@ -14,24 +14,27 @@ class RowController {
|
||||
|
||||
get cellCache => _rowCache.cellCache;
|
||||
|
||||
get rowId => rowMeta.id;
|
||||
|
||||
RowController({
|
||||
required this.rowId,
|
||||
required this.rowMeta,
|
||||
required this.viewId,
|
||||
required RowCache rowCache,
|
||||
this.groupId,
|
||||
}) : _rowCache = rowCache;
|
||||
|
||||
CellContextByFieldId loadData() {
|
||||
return _rowCache.loadGridCells(rowId);
|
||||
return _rowCache.loadGridCells(rowMeta);
|
||||
}
|
||||
|
||||
void addListener({OnRowChanged? onRowChanged}) {
|
||||
_onRowChangedListeners.add(
|
||||
_rowCache.addListener(
|
||||
rowId: rowId,
|
||||
onCellUpdated: onRowChanged,
|
||||
),
|
||||
final fn = _rowCache.addListener(
|
||||
rowId: rowMeta.id,
|
||||
onCellUpdated: onRowChanged,
|
||||
);
|
||||
|
||||
// Add the listener to the list so that we can remove it later.
|
||||
_onRowChangedListeners.add(fn);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@@ -25,10 +25,9 @@ class RowList {
|
||||
}
|
||||
|
||||
void add(RowInfo rowInfo) {
|
||||
final rowId = rowInfo.rowPB.id;
|
||||
final rowId = rowInfo.rowId;
|
||||
if (contains(rowId)) {
|
||||
final index =
|
||||
_rowInfos.indexWhere((element) => element.rowPB.id == rowId);
|
||||
final index = _rowInfos.indexWhere((element) => element.rowId == rowId);
|
||||
_rowInfos.removeAt(index);
|
||||
_rowInfos.insert(index, rowInfo);
|
||||
} else {
|
||||
@@ -38,7 +37,7 @@ class RowList {
|
||||
}
|
||||
|
||||
InsertedIndex? insert(int index, RowInfo rowInfo) {
|
||||
final rowId = rowInfo.rowPB.id;
|
||||
final rowId = rowInfo.rowId;
|
||||
var insertedIndex = index;
|
||||
if (_rowInfos.length <= insertedIndex) {
|
||||
insertedIndex = _rowInfos.length;
|
||||
@@ -62,7 +61,7 @@ class RowList {
|
||||
if (rowInfo != null) {
|
||||
final index = _rowInfos.indexOf(rowInfo);
|
||||
if (index != -1) {
|
||||
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
||||
rowInfoByRowId.remove(rowInfo.rowId);
|
||||
_rowInfos.remove(rowInfo);
|
||||
}
|
||||
return DeletedIndex(index: index, rowInfo: rowInfo);
|
||||
@@ -73,23 +72,23 @@ class RowList {
|
||||
|
||||
InsertedIndexs insertRows(
|
||||
List<InsertedRowPB> insertedRows,
|
||||
RowInfo Function(RowPB) builder,
|
||||
RowInfo Function(RowMetaPB) builder,
|
||||
) {
|
||||
final InsertedIndexs insertIndexs = [];
|
||||
for (final insertRow in insertedRows) {
|
||||
final isContains = contains(insertRow.row.id);
|
||||
final isContains = contains(insertRow.rowMeta.id);
|
||||
|
||||
var index = insertRow.index;
|
||||
if (_rowInfos.length < index) {
|
||||
index = _rowInfos.length;
|
||||
}
|
||||
insert(index, builder(insertRow.row));
|
||||
insert(index, builder(insertRow.rowMeta));
|
||||
|
||||
if (!isContains) {
|
||||
insertIndexs.add(
|
||||
InsertedIndex(
|
||||
index: index,
|
||||
rowId: insertRow.row.id,
|
||||
rowId: insertRow.rowMeta.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -105,10 +104,10 @@ class RowList {
|
||||
};
|
||||
|
||||
_rowInfos.asMap().forEach((index, RowInfo rowInfo) {
|
||||
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
|
||||
if (deletedRowByRowId[rowInfo.rowId] == null) {
|
||||
newRows.add(rowInfo);
|
||||
} else {
|
||||
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
||||
rowInfoByRowId.remove(rowInfo.rowId);
|
||||
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
|
||||
}
|
||||
});
|
||||
@@ -117,19 +116,21 @@ class RowList {
|
||||
}
|
||||
|
||||
UpdatedIndexMap updateRows(
|
||||
List<RowPB> updatedRows,
|
||||
RowInfo Function(RowPB) builder,
|
||||
List<RowMetaPB> rowMetas,
|
||||
RowInfo Function(RowMetaPB) builder,
|
||||
) {
|
||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||
for (final RowPB updatedRow in updatedRows) {
|
||||
final rowId = updatedRow.id;
|
||||
for (final rowMeta in rowMetas) {
|
||||
final index = _rowInfos.indexWhere(
|
||||
(rowInfo) => rowInfo.rowPB.id == rowId,
|
||||
(rowInfo) => rowInfo.rowId == rowMeta.id,
|
||||
);
|
||||
if (index != -1) {
|
||||
final rowInfo = builder(updatedRow);
|
||||
final rowInfo = builder(rowMeta);
|
||||
insert(index, rowInfo);
|
||||
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
|
||||
updatedIndexs[rowMeta.id] = UpdatedIndex(
|
||||
index: index,
|
||||
rowId: rowMeta.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
return updatedIndexs;
|
||||
@@ -148,7 +149,7 @@ class RowList {
|
||||
|
||||
void moveRow(RowId rowId, int oldIndex, int newIndex) {
|
||||
final index = _rowInfos.indexWhere(
|
||||
(rowInfo) => rowInfo.rowPB.id == rowId,
|
||||
(rowInfo) => rowInfo.rowId == rowId,
|
||||
);
|
||||
if (index != -1) {
|
||||
final rowInfo = remove(rowId)!.rowInfo;
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
typedef RowMetaCallback = void Function(RowMetaPB);
|
||||
|
||||
class RowMetaListener {
|
||||
final String rowId;
|
||||
RowMetaCallback? _callback;
|
||||
DatabaseNotificationListener? _listener;
|
||||
RowMetaListener(this.rowId);
|
||||
|
||||
void start({required RowMetaCallback callback}) {
|
||||
_callback = callback;
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: rowId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
DatabaseNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateRowMeta:
|
||||
result.fold(
|
||||
(payload) {
|
||||
if (_callback != null) {
|
||||
_callback!(RowMetaPB.fromBuffer(payload));
|
||||
}
|
||||
},
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_callback = null;
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -12,7 +12,7 @@ class RowBackendService {
|
||||
required this.viewId,
|
||||
});
|
||||
|
||||
Future<Either<RowPB, FlowyError>> createRow(RowId rowId) {
|
||||
Future<Either<RowMetaPB, FlowyError>> createRowAfterRow(RowId rowId) {
|
||||
final payload = CreateRowPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..startRowId = rowId;
|
||||
@@ -28,6 +28,33 @@ class RowBackendService {
|
||||
return DatabaseEventGetRow(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<RowMetaPB, FlowyError>> getRowMeta(RowId rowId) {
|
||||
final payload = RowIdPB.create()
|
||||
..viewId = viewId
|
||||
..rowId = rowId;
|
||||
|
||||
return DatabaseEventGetRowMeta(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateMeta({
|
||||
required String rowId,
|
||||
String? iconURL,
|
||||
String? coverURL,
|
||||
}) {
|
||||
final payload = UpdateRowMetaChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..id = rowId;
|
||||
|
||||
if (iconURL != null) {
|
||||
payload.iconUrl = iconURL;
|
||||
}
|
||||
if (coverURL != null) {
|
||||
payload.coverUrl = coverURL;
|
||||
}
|
||||
|
||||
return DatabaseEventUpdateRowMeta(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> deleteRow(RowId rowId) {
|
||||
final payload = RowIdPB.create()
|
||||
..viewId = viewId
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
|
||||
setGroupByField: (String fieldId, FieldType fieldType) async {
|
||||
final result = await _groupBackendSvc.groupByField(
|
||||
fieldId: fieldId,
|
||||
fieldType: fieldType,
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.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,
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
-17
@@ -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,20 +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.row.id).toList());
|
||||
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.row.id)
|
||||
.toList(),
|
||||
);
|
||||
for (final callback in _callbacks) {
|
||||
callback.onRowsCreated?.call(
|
||||
changeset.insertedRows
|
||||
.map((insertedRow) => insertedRow.rowMeta.id)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
@@ -101,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);
|
||||
}
|
||||
}
|
||||
+16
-18
@@ -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,
|
||||
@@ -156,7 +155,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
|
||||
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@@ -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();
|
||||
}
|
||||
@@ -230,12 +228,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
for (final group in updatedGroups) {
|
||||
final columnController =
|
||||
boardController.getGroupController(group.groupId);
|
||||
columnController?.updateGroupName(group.desc);
|
||||
columnController?.updateGroupName(group.groupName);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
databaseController.setListener(
|
||||
databaseController.addListener(
|
||||
onDatabaseChanged: onDatabaseChanged,
|
||||
onGroupChanged: onGroupChanged,
|
||||
);
|
||||
@@ -285,7 +283,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
AppFlowyGroupData initializeGroupData(GroupPB group) {
|
||||
return AppFlowyGroupData(
|
||||
id: group.groupId,
|
||||
name: group.desc,
|
||||
name: group.groupName,
|
||||
items: _buildGroupItems(group),
|
||||
customData: GroupData(
|
||||
group: group,
|
||||
@@ -302,12 +300,12 @@ class BoardEvent with _$BoardEvent {
|
||||
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
|
||||
const factory BoardEvent.didCreateRow(
|
||||
GroupPB group,
|
||||
RowPB row,
|
||||
RowMetaPB row,
|
||||
int? index,
|
||||
) = _DidCreateRow;
|
||||
const factory BoardEvent.startEditingRow(
|
||||
GroupPB group,
|
||||
RowPB row,
|
||||
RowMetaPB row,
|
||||
) = _StartEditRow;
|
||||
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
|
||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
||||
@@ -371,7 +369,7 @@ class GridFieldEquatable extends Equatable {
|
||||
}
|
||||
|
||||
class GroupItem extends AppFlowyGroupItem {
|
||||
final RowPB row;
|
||||
final RowMetaPB row;
|
||||
final FieldInfo fieldInfo;
|
||||
|
||||
GroupItem({
|
||||
@@ -389,7 +387,7 @@ class GroupItem extends AppFlowyGroupItem {
|
||||
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
final FieldController fieldController;
|
||||
final AppFlowyBoardController controller;
|
||||
final void Function(String, RowPB, int?) onNewColumnItem;
|
||||
final void Function(String, RowMetaPB, int?) onNewColumnItem;
|
||||
|
||||
GroupControllerDelegateImpl({
|
||||
required this.controller,
|
||||
@@ -398,7 +396,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
});
|
||||
|
||||
@override
|
||||
void insertRow(GroupPB group, RowPB row, int? index) {
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@@ -426,7 +424,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRow(GroupPB group, RowPB row) {
|
||||
void updateRow(GroupPB group, RowMetaPB row) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@@ -442,7 +440,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
void addNewRow(GroupPB group, RowPB row, int? index) {
|
||||
void addNewRow(GroupPB group, RowMetaPB row, int? index) {
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
@@ -465,7 +463,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
|
||||
class BoardEditingRow {
|
||||
GroupPB group;
|
||||
RowPB row;
|
||||
RowMetaPB row;
|
||||
int? index;
|
||||
|
||||
BoardEditingRow({
|
||||
|
||||
+9
-9
@@ -12,9 +12,9 @@ typedef OnGroupError = void Function(FlowyError);
|
||||
|
||||
abstract class GroupControllerDelegate {
|
||||
void removeRow(GroupPB group, RowId rowId);
|
||||
void insertRow(GroupPB group, RowPB row, int? index);
|
||||
void updateRow(GroupPB group, RowPB row);
|
||||
void addNewRow(GroupPB group, RowPB row, int? index);
|
||||
void insertRow(GroupPB group, RowMetaPB row, int? index);
|
||||
void updateRow(GroupPB group, RowMetaPB row);
|
||||
void addNewRow(GroupPB group, RowMetaPB row, int? index);
|
||||
}
|
||||
|
||||
class GroupController {
|
||||
@@ -28,7 +28,7 @@ class GroupController {
|
||||
required this.delegate,
|
||||
}) : _listener = SingleGroupListener(group);
|
||||
|
||||
RowPB? rowAtIndex(int index) {
|
||||
RowMetaPB? rowAtIndex(int index) {
|
||||
if (index < group.rows.length) {
|
||||
return group.rows[index];
|
||||
} else {
|
||||
@@ -36,7 +36,7 @@ class GroupController {
|
||||
}
|
||||
}
|
||||
|
||||
RowPB? lastRow() {
|
||||
RowMetaPB? lastRow() {
|
||||
if (group.rows.isEmpty) return null;
|
||||
return group.rows.last;
|
||||
}
|
||||
@@ -55,15 +55,15 @@ class GroupController {
|
||||
final index = insertedRow.hasIndex() ? insertedRow.index : null;
|
||||
if (insertedRow.hasIndex() &&
|
||||
group.rows.length > insertedRow.index) {
|
||||
group.rows.insert(insertedRow.index, insertedRow.row);
|
||||
group.rows.insert(insertedRow.index, insertedRow.rowMeta);
|
||||
} else {
|
||||
group.rows.add(insertedRow.row);
|
||||
group.rows.add(insertedRow.rowMeta);
|
||||
}
|
||||
|
||||
if (insertedRow.isNew) {
|
||||
delegate.addNewRow(group, insertedRow.row, index);
|
||||
delegate.addNewRow(group, insertedRow.rowMeta, index);
|
||||
} else {
|
||||
delegate.insertRow(group, insertedRow.row, index);
|
||||
delegate.insertRow(group, insertedRow.rowMeta, index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,51 +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,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(view: view);
|
||||
|
||||
@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];
|
||||
}
|
||||
+69
-44
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -231,7 +266,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
) {
|
||||
final groupItem = afGroupItem as GroupItem;
|
||||
final groupData = afGroupData.customData as GroupData;
|
||||
final rowPB = groupItem.row;
|
||||
final rowMeta = groupItem.row;
|
||||
final rowCache = context.read<BoardBloc>().getRowCache();
|
||||
|
||||
/// Return placeholder widget if the rowCache is null.
|
||||
@@ -255,7 +290,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
margin: config.cardPadding,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: RowCard<String>(
|
||||
row: rowPB,
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: groupData.group.groupId,
|
||||
@@ -267,7 +302,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
viewId,
|
||||
groupData.group.groupId,
|
||||
fieldController,
|
||||
rowPB,
|
||||
rowMeta,
|
||||
rowCache,
|
||||
context,
|
||||
),
|
||||
@@ -305,18 +340,19 @@ class _BoardContentState extends State<BoardContent> {
|
||||
String viewId,
|
||||
String groupId,
|
||||
FieldController fieldController,
|
||||
RowPB rowPB,
|
||||
RowMetaPB rowMetaPB,
|
||||
RowCache rowCache,
|
||||
BuildContext context,
|
||||
) {
|
||||
final rowInfo = RowInfo(
|
||||
viewId: viewId,
|
||||
fields: UnmodifiableListView(fieldController.fieldInfos),
|
||||
rowPB: rowPB,
|
||||
rowMeta: rowMetaPB,
|
||||
rowId: rowMetaPB.id,
|
||||
);
|
||||
|
||||
final dataController = RowController(
|
||||
rowId: rowInfo.rowPB.id,
|
||||
rowMeta: rowInfo.rowMeta,
|
||||
viewId: rowInfo.viewId,
|
||||
rowCache: rowCache,
|
||||
groupId: groupId,
|
||||
@@ -334,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),
|
||||
],
|
||||
),
|
||||
);
|
||||
+15
-34
@@ -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 {
|
||||
@@ -268,7 +265,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
|
||||
final eventData = CalendarDayEvent(
|
||||
event: eventPB,
|
||||
eventId: eventPB.rowId,
|
||||
eventId: eventPB.rowMeta.id,
|
||||
dateFieldId: eventPB.dateFieldId,
|
||||
date: date,
|
||||
);
|
||||
@@ -310,7 +307,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
}
|
||||
add(CalendarEvent.didDeleteEvents(rowIds));
|
||||
},
|
||||
onRowsUpdated: (rowIds) async {
|
||||
onRowsUpdated: (rowIds, reason) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+51
-17
@@ -1,3 +1,5 @@
|
||||
import 'package:appflowy/plugins/database_view/application/layout/layout_setting_listener.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@@ -9,10 +11,19 @@ typedef DayOfWeek = int;
|
||||
|
||||
class CalendarSettingBloc
|
||||
extends Bloc<CalendarSettingEvent, CalendarSettingState> {
|
||||
CalendarSettingBloc({required CalendarLayoutSettingPB? layoutSettings})
|
||||
: super(CalendarSettingState.initial(layoutSettings)) {
|
||||
final String viewId;
|
||||
final DatabaseLayoutSettingListener _listener;
|
||||
|
||||
CalendarSettingBloc({
|
||||
required this.viewId,
|
||||
required CalendarLayoutSettingPB? layoutSettings,
|
||||
}) : _listener = DatabaseLayoutSettingListener(viewId),
|
||||
super(CalendarSettingState.initial(layoutSettings)) {
|
||||
on<CalendarSettingEvent>((event, emit) {
|
||||
event.when(
|
||||
init: () {
|
||||
_startListening();
|
||||
},
|
||||
performAction: (action) {
|
||||
emit(state.copyWith(selectedAction: Some(action)));
|
||||
},
|
||||
@@ -22,6 +33,44 @@ class CalendarSettingBloc
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onLayoutChanged: (result) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(setting) =>
|
||||
add(CalendarSettingEvent.updateLayoutSetting(setting.calendar)),
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalendarSettingEvent with _$CalendarSettingEvent {
|
||||
const factory CalendarSettingEvent.init() = _Init;
|
||||
const factory CalendarSettingEvent.performAction(
|
||||
CalendarSettingAction action,
|
||||
) = _PerformAction;
|
||||
const factory CalendarSettingEvent.updateLayoutSetting(
|
||||
CalendarLayoutSettingPB setting,
|
||||
) = _UpdateLayoutSetting;
|
||||
}
|
||||
|
||||
enum CalendarSettingAction {
|
||||
properties,
|
||||
layout,
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -39,18 +88,3 @@ class CalendarSettingState with _$CalendarSettingState {
|
||||
layoutSetting: layoutSettings == null ? none() : Some(layoutSettings),
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalendarSettingEvent with _$CalendarSettingEvent {
|
||||
const factory CalendarSettingEvent.performAction(
|
||||
CalendarSettingAction action,
|
||||
) = _PerformAction;
|
||||
const factory CalendarSettingEvent.updateLayoutSetting(
|
||||
CalendarLayoutSettingPB setting,
|
||||
) = _UpdateLayoutSetting;
|
||||
}
|
||||
|
||||
enum CalendarSettingAction {
|
||||
properties,
|
||||
layout,
|
||||
}
|
||||
+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,51 +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,
|
||||
}) : _pluginType = pluginType,
|
||||
notifier = ViewPluginNotifier(view: view);
|
||||
|
||||
@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];
|
||||
}
|
||||
+2
-2
@@ -301,7 +301,7 @@ class _EventCard extends StatelessWidget {
|
||||
// Add the key here to make sure the card is rebuilt when the cells
|
||||
// in this row are updated.
|
||||
key: ValueKey(event.eventId),
|
||||
row: rowInfo!.rowPB,
|
||||
rowMeta: rowInfo!.rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: event.dateFieldId,
|
||||
@@ -309,7 +309,7 @@ class _EventCard extends StatelessWidget {
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) => showEventDetails(
|
||||
context: context,
|
||||
event: event,
|
||||
event: event.event,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
|
||||
+65
-18
@@ -1,5 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -15,11 +19,51 @@ import '../../widgets/row/cell_builder.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_toolbar.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
|
||||
class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
ViewPB view,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return CalendarPage(
|
||||
key: _makeValueKey(controller),
|
||||
view: view,
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBar(BuildContext context, DatabaseController controller) {
|
||||
return CalendarSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget settingBarExtension(
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return SizedBox.fromSize();
|
||||
}
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
return ValueKey(controller.viewId);
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarPage extends StatefulWidget {
|
||||
final ViewPB view;
|
||||
const CalendarPage({required this.view, super.key});
|
||||
final DatabaseController databaseController;
|
||||
const CalendarPage({
|
||||
required this.view,
|
||||
required this.databaseController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CalendarPage> createState() => _CalendarPageState();
|
||||
@@ -33,8 +77,10 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
@override
|
||||
void initState() {
|
||||
_calendarState = GlobalKey<MonthViewState>();
|
||||
_calendarBloc = CalendarBloc(view: widget.view)
|
||||
..add(const CalendarEvent.initial());
|
||||
_calendarBloc = CalendarBloc(
|
||||
view: widget.view,
|
||||
databaseController: widget.databaseController,
|
||||
)..add(const CalendarEvent.initial());
|
||||
|
||||
super.initState();
|
||||
}
|
||||
@@ -79,7 +125,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
if (state.editingEvent != null) {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: state.editingEvent!.event!,
|
||||
event: state.editingEvent!.event!.event,
|
||||
viewId: widget.view.id,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
);
|
||||
@@ -115,8 +161,6 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
// const _ToolbarBlocAdaptor(),
|
||||
const CalendarToolbar(),
|
||||
_buildCalendar(
|
||||
_eventController,
|
||||
state.settings
|
||||
@@ -133,15 +177,18 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
Widget _buildCalendar(EventController eventController, int firstDayOfWeek) {
|
||||
return Expanded(
|
||||
child: MonthView(
|
||||
key: _calendarState,
|
||||
controller: _eventController,
|
||||
cellAspectRatio: .6,
|
||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||
borderColor: Theme.of(context).dividerColor,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
weekDayBuilder: _headerWeekDayBuilder,
|
||||
cellBuilder: _calendarDayBuilder,
|
||||
child: Padding(
|
||||
padding: GridSize.contentInsets,
|
||||
child: MonthView(
|
||||
key: _calendarState,
|
||||
controller: _eventController,
|
||||
cellAspectRatio: .6,
|
||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||
borderColor: Theme.of(context).dividerColor,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
weekDayBuilder: _headerWeekDayBuilder,
|
||||
cellBuilder: _calendarDayBuilder,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -238,12 +285,12 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
void showEventDetails({
|
||||
required BuildContext context,
|
||||
required CalendarDayEvent event,
|
||||
required CalendarEventPB event,
|
||||
required String viewId,
|
||||
required RowCache rowCache,
|
||||
}) {
|
||||
final dataController = RowController(
|
||||
rowId: event.eventId,
|
||||
rowMeta: event.rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
+135
-101
@@ -15,17 +15,25 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
import 'calendar_setting.dart';
|
||||
abstract class ICalendarSetting {
|
||||
/// Returns the current layout settings for the calendar view.
|
||||
CalendarLayoutSettingPB? getLayoutSetting();
|
||||
|
||||
/// Updates the layout settings for the calendar view.
|
||||
void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings);
|
||||
}
|
||||
|
||||
/// Widget that displays a list of settings that alters the appearance of the
|
||||
/// calendar
|
||||
class CalendarLayoutSetting extends StatefulWidget {
|
||||
final CalendarSettingContext settingContext;
|
||||
final Function(CalendarLayoutSettingPB? layoutSettings) onUpdated;
|
||||
final String viewId;
|
||||
final FieldController fieldController;
|
||||
final ICalendarSetting calendarSettingController;
|
||||
|
||||
const CalendarLayoutSetting({
|
||||
required this.onUpdated,
|
||||
required this.settingContext,
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
required this.calendarSettingController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -44,85 +52,90 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
|
||||
builder: (context, state) {
|
||||
final CalendarLayoutSettingPB? settings = state.layoutSetting
|
||||
.foldLeft(null, (previous, settings) => settings);
|
||||
|
||||
if (settings == null) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
final availableSettings = _availableCalendarSettings(settings);
|
||||
|
||||
final items = availableSettings.map((setting) {
|
||||
switch (setting) {
|
||||
case CalendarLayoutSettingAction.showWeekNumber:
|
||||
return ShowWeekNumber(
|
||||
showWeekNumbers: settings.showWeekNumbers,
|
||||
onUpdated: (showWeekNumbers) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
showWeekNumbers: showWeekNumbers,
|
||||
onUpdated: widget.onUpdated,
|
||||
);
|
||||
},
|
||||
);
|
||||
case CalendarLayoutSettingAction.showWeekends:
|
||||
return ShowWeekends(
|
||||
showWeekends: settings.showWeekends,
|
||||
onUpdated: (showWeekends) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
showWeekends: showWeekends,
|
||||
onUpdated: widget.onUpdated,
|
||||
);
|
||||
},
|
||||
);
|
||||
case CalendarLayoutSettingAction.firstDayOfWeek:
|
||||
return FirstDayOfWeek(
|
||||
firstDayOfWeek: settings.firstDayOfWeek,
|
||||
popoverMutex: popoverMutex,
|
||||
onUpdated: (firstDayOfWeek) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
onUpdated: widget.onUpdated,
|
||||
firstDayOfWeek: firstDayOfWeek,
|
||||
);
|
||||
},
|
||||
);
|
||||
case CalendarLayoutSettingAction.layoutField:
|
||||
return LayoutDateField(
|
||||
fieldController: widget.settingContext.fieldController,
|
||||
viewId: widget.settingContext.viewId,
|
||||
fieldId: settings.fieldId,
|
||||
popoverMutex: popoverMutex,
|
||||
onUpdated: (fieldId) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
onUpdated: widget.onUpdated,
|
||||
layoutFieldId: fieldId,
|
||||
);
|
||||
},
|
||||
);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
controller: ScrollController(),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) => items[index],
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
),
|
||||
);
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return CalendarSettingBloc(
|
||||
viewId: widget.viewId,
|
||||
layoutSettings: widget.calendarSettingController.getLayoutSetting(),
|
||||
)..add(
|
||||
const CalendarSettingEvent.init(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
|
||||
builder: (context, state) {
|
||||
final CalendarLayoutSettingPB? settings = state.layoutSetting
|
||||
.foldLeft(null, (previous, settings) => settings);
|
||||
|
||||
if (settings == null) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
final availableSettings = _availableCalendarSettings(settings);
|
||||
final items = availableSettings.map((setting) {
|
||||
switch (setting) {
|
||||
case CalendarLayoutSettingAction.showWeekNumber:
|
||||
return ShowWeekNumber(
|
||||
showWeekNumbers: settings.showWeekNumbers,
|
||||
onUpdated: (showWeekNumbers) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
showWeekNumbers: showWeekNumbers,
|
||||
);
|
||||
},
|
||||
);
|
||||
case CalendarLayoutSettingAction.showWeekends:
|
||||
return ShowWeekends(
|
||||
showWeekends: settings.showWeekends,
|
||||
onUpdated: (showWeekends) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
showWeekends: showWeekends,
|
||||
);
|
||||
},
|
||||
);
|
||||
case CalendarLayoutSettingAction.firstDayOfWeek:
|
||||
return FirstDayOfWeek(
|
||||
firstDayOfWeek: settings.firstDayOfWeek,
|
||||
popoverMutex: popoverMutex,
|
||||
onUpdated: (firstDayOfWeek) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
firstDayOfWeek: firstDayOfWeek,
|
||||
);
|
||||
},
|
||||
);
|
||||
case CalendarLayoutSettingAction.layoutField:
|
||||
return LayoutDateField(
|
||||
fieldController: widget.fieldController,
|
||||
viewId: widget.viewId,
|
||||
fieldId: settings.fieldId,
|
||||
popoverMutex: popoverMutex,
|
||||
onUpdated: (fieldId) {
|
||||
_updateLayoutSettings(
|
||||
context,
|
||||
layoutFieldId: fieldId,
|
||||
);
|
||||
},
|
||||
);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
controller: ScrollController(),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) => items[index],
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,7 +174,6 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
|
||||
|
||||
void _updateLayoutSettings(
|
||||
BuildContext context, {
|
||||
required Function(CalendarLayoutSettingPB? layoutSettings) onUpdated,
|
||||
bool? showWeekends,
|
||||
bool? showWeekNumbers,
|
||||
int? firstDayOfWeek,
|
||||
@@ -190,7 +202,8 @@ class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
|
||||
context
|
||||
.read<CalendarSettingBloc>()
|
||||
.add(CalendarSettingEvent.updateLayoutSetting(setting));
|
||||
onUpdated(setting);
|
||||
|
||||
widget.calendarSettingController.updateLayoutSettings(setting);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,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();
|
||||
|
||||
@@ -363,7 +371,7 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
itemBuilder: (context, index) => items[index],
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
itemCount: 2,
|
||||
itemCount: len,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -413,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
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/calendar/application/calendar_setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import 'calendar_layout_setting.dart';
|
||||
|
||||
/// The highest-level widget shown in the popover triggered by clicking the
|
||||
/// "Settings" button. Shows [AllCalendarSettings] by default, but replaces its
|
||||
/// contents with the submenu when a category is selected.
|
||||
class CalendarSetting extends StatelessWidget {
|
||||
final CalendarSettingContext settingContext;
|
||||
final CalendarLayoutSettingPB? layoutSettings;
|
||||
final Function(CalendarLayoutSettingPB? layoutSettings) onUpdated;
|
||||
|
||||
const CalendarSetting({
|
||||
required this.onUpdated,
|
||||
required this.layoutSettings,
|
||||
required this.settingContext,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<CalendarSettingBloc>(
|
||||
create: (context) => CalendarSettingBloc(layoutSettings: layoutSettings),
|
||||
child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
|
||||
builder: (context, state) {
|
||||
final CalendarSettingAction? action =
|
||||
state.selectedAction.foldLeft(null, (previous, action) => action);
|
||||
switch (action) {
|
||||
case CalendarSettingAction.properties:
|
||||
return DatabasePropertyList(
|
||||
viewId: settingContext.viewId,
|
||||
fieldController: settingContext.fieldController,
|
||||
);
|
||||
case CalendarSettingAction.layout:
|
||||
return CalendarLayoutSetting(
|
||||
onUpdated: onUpdated,
|
||||
settingContext: settingContext,
|
||||
);
|
||||
default:
|
||||
return const AllCalendarSettings().padding(all: 6.0);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows all of the available categories of settings that can be set here.
|
||||
/// For now, this only includes the Layout category.
|
||||
class AllCalendarSettings extends StatelessWidget {
|
||||
const AllCalendarSettings({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = CalendarSettingAction.values
|
||||
.map((e) => _settingItem(context, e))
|
||||
.toList();
|
||||
|
||||
return SizedBox(
|
||||
width: 140,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
controller: ScrollController(),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) => items[index],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _settingItem(BuildContext context, CalendarSettingAction action) {
|
||||
Widget? icon;
|
||||
if (action.iconName() != null) {
|
||||
icon = FlowySvg(
|
||||
name: action.iconName()!,
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
leftIcon: icon,
|
||||
text: FlowyText.medium(action.title()),
|
||||
onTap: () {
|
||||
context
|
||||
.read<CalendarSettingBloc>()
|
||||
.add(CalendarSettingEvent.performAction(action));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension _SettingExtension on CalendarSettingAction {
|
||||
String? iconName() {
|
||||
switch (this) {
|
||||
case CalendarSettingAction.properties:
|
||||
return 'grid/setting/properties';
|
||||
case CalendarSettingAction.layout:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String title() {
|
||||
switch (this) {
|
||||
case CalendarSettingAction.properties:
|
||||
return LocaleKeys.grid_settings_Properties.tr();
|
||||
case CalendarSettingAction.layout:
|
||||
return LocaleKeys.grid_settings_layout.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarSettingContext {
|
||||
final String viewId;
|
||||
final FieldController fieldController;
|
||||
|
||||
CalendarSettingContext({
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
});
|
||||
}
|
||||
+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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-183
@@ -1,183 +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';
|
||||
import 'calendar_setting.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 _SettingButton extends StatefulWidget {
|
||||
const _SettingButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SettingButtonState();
|
||||
}
|
||||
|
||||
class _SettingButtonState extends State<_SettingButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
constraints: BoxConstraints.loose(const Size(300, 400)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: FlowyTextButton(
|
||||
LocaleKeys.settings_title.tr(),
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
final bloc = context.watch<CalendarBloc>();
|
||||
final settingContext = CalendarSettingContext(
|
||||
viewId: bloc.viewId,
|
||||
fieldController: bloc.fieldController,
|
||||
);
|
||||
return CalendarSetting(
|
||||
settingContext: settingContext,
|
||||
layoutSettings: bloc.state.settings.fold(
|
||||
() => null,
|
||||
(settings) => settings,
|
||||
),
|
||||
onUpdated: (layoutSettings) {
|
||||
if (layoutSettings == null) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<CalendarBloc>()
|
||||
.add(CalendarEvent.updateCalendarLayoutSetting(layoutSettings));
|
||||
},
|
||||
);
|
||||
}, // use blocbuilder
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import '../../workspace/presentation/home/home_stack.dart';
|
||||
|
||||
/// [DatabaseViewPlugin] is used to build the grid, calendar, and board.
|
||||
/// It is a wrapper of the [Plugin] class. The underlying [Plugin] is
|
||||
/// determined by the [ViewPB.pluginType] field.
|
||||
///
|
||||
class DatabaseViewPlugin extends Plugin {
|
||||
final ViewListener _viewListener;
|
||||
ViewPB _view;
|
||||
Plugin _innerPlugin;
|
||||
|
||||
DatabaseViewPlugin({
|
||||
required ViewPB view,
|
||||
}) : _view = view,
|
||||
_innerPlugin = _makeInnerPlugin(view),
|
||||
_viewListener = ViewListener(view: view) {
|
||||
_listenOnLayoutChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
PluginId get id => _innerPlugin.id;
|
||||
|
||||
@override
|
||||
PluginType get pluginType => _innerPlugin.pluginType;
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder => _innerPlugin.widgetBuilder;
|
||||
|
||||
void _listenOnLayoutChanged() {
|
||||
_viewListener.start(
|
||||
onViewUpdated: (result) {
|
||||
result.fold(
|
||||
(updatedView) {
|
||||
if (_view.layout != updatedView.layout) {
|
||||
_innerPlugin = _makeInnerPlugin(updatedView);
|
||||
|
||||
getIt<HomeStackManager>().setPlugin(_innerPlugin);
|
||||
}
|
||||
_view = updatedView;
|
||||
},
|
||||
(r) => null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Plugin _makeInnerPlugin(ViewPB view) {
|
||||
return makePlugin(pluginType: view.pluginType, data: view);
|
||||
}
|
||||
+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,
|
||||
);
|
||||
|
||||
+56
-16
@@ -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';
|
||||
@@ -33,18 +35,18 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
final rowService = RowBackendService(
|
||||
viewId: rowInfo.viewId,
|
||||
);
|
||||
await rowService.deleteRow(rowInfo.rowPB.id);
|
||||
await rowService.deleteRow(rowInfo.rowId);
|
||||
},
|
||||
moveRow: (int from, int to) {
|
||||
final List<RowInfo> rows = [...state.rowInfos];
|
||||
|
||||
final fromRow = rows[from].rowPB;
|
||||
final toRow = rows[to].rowPB;
|
||||
final fromRow = rows[from].rowId;
|
||||
final toRow = rows[to].rowId;
|
||||
|
||||
rows.insert(to, rows.removeAt(from));
|
||||
emit(state.copyWith(rowInfos: rows));
|
||||
|
||||
databaseController.moveRow(fromRow: fromRow, toRow: toRow);
|
||||
databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);
|
||||
},
|
||||
didReceiveGridUpdate: (grid) {
|
||||
emit(state.copyWith(grid: Some(grid)));
|
||||
@@ -56,7 +58,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveRowUpdate: (newRowInfos, reason) {
|
||||
didLoadRows: (newRowInfos, reason) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
rowInfos: newRowInfos,
|
||||
@@ -65,18 +67,28 @@ 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) {
|
||||
RowCache getRowCache(RowId rowId) {
|
||||
return databaseController.rowCache;
|
||||
}
|
||||
|
||||
@@ -89,7 +101,14 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
},
|
||||
onNumOfRowsChanged: (rowInfos, _, reason) {
|
||||
if (!isClosed) {
|
||||
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
|
||||
add(GridEvent.didLoadRows(rowInfos, reason));
|
||||
}
|
||||
},
|
||||
onRowsUpdated: (rows, reason) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFieldsChanged: (fields) {
|
||||
@@ -97,8 +116,18 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
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 {
|
||||
@@ -122,9 +151,9 @@ class GridEvent with _$GridEvent {
|
||||
const factory GridEvent.createRow() = _CreateRow;
|
||||
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
|
||||
const factory GridEvent.moveRow(int from, int to) = _MoveRow;
|
||||
const factory GridEvent.didReceiveRowUpdate(
|
||||
const factory GridEvent.didLoadRows(
|
||||
List<RowInfo> rows,
|
||||
RowsChangedReason listState,
|
||||
RowsChangedReason reason,
|
||||
) = _DidReceiveRowUpdate;
|
||||
const factory GridEvent.didReceiveFieldUpdate(
|
||||
List<FieldInfo> fields,
|
||||
@@ -133,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
|
||||
@@ -144,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(
|
||||
@@ -153,8 +190,11 @@ class GridState with _$GridState {
|
||||
rowCount: 0,
|
||||
grid: none(),
|
||||
viewId: viewId,
|
||||
reorderable: true,
|
||||
loadingState: const _Loading(),
|
||||
reason: const InitialListState(),
|
||||
filters: [],
|
||||
sorts: [],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import '../../../application/row/row_cache.dart';
|
||||
import '../../../application/row/row_service.dart';
|
||||
|
||||
part 'row_action_sheet_bloc.freezed.dart';
|
||||
@@ -13,19 +12,20 @@ class RowActionSheetBloc
|
||||
extends Bloc<RowActionSheetEvent, RowActionSheetState> {
|
||||
final RowBackendService _rowService;
|
||||
|
||||
RowActionSheetBloc({required RowInfo rowInfo})
|
||||
: _rowService = RowBackendService(viewId: rowInfo.viewId),
|
||||
super(RowActionSheetState.initial(rowInfo)) {
|
||||
RowActionSheetBloc({
|
||||
required String viewId,
|
||||
required RowId rowId,
|
||||
}) : _rowService = RowBackendService(viewId: viewId),
|
||||
super(RowActionSheetState.initial(rowId)) {
|
||||
on<RowActionSheetEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
deleteRow: () async {
|
||||
final result = await _rowService.deleteRow(state.rowData.rowPB.id);
|
||||
final result = await _rowService.deleteRow(state.rowId);
|
||||
logResult(result);
|
||||
},
|
||||
duplicateRow: () async {
|
||||
final result =
|
||||
await _rowService.duplicateRow(rowId: state.rowData.rowPB.id);
|
||||
final result = await _rowService.duplicateRow(rowId: state.rowId);
|
||||
logResult(result);
|
||||
},
|
||||
);
|
||||
@@ -47,10 +47,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent {
|
||||
@freezed
|
||||
class RowActionSheetState with _$RowActionSheetState {
|
||||
const factory RowActionSheetState({
|
||||
required RowInfo rowData,
|
||||
required RowId rowId,
|
||||
}) = _RowActionSheetState;
|
||||
|
||||
factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState(
|
||||
rowData: rowData,
|
||||
factory RowActionSheetState.initial(RowId rowId) => RowActionSheetState(
|
||||
rowId: rowId,
|
||||
);
|
||||
}
|
||||
+7
-7
@@ -15,13 +15,16 @@ part 'row_bloc.freezed.dart';
|
||||
class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
final RowBackendService _rowBackendSvc;
|
||||
final RowController _dataController;
|
||||
final String viewId;
|
||||
final String rowId;
|
||||
|
||||
RowBloc({
|
||||
required RowInfo rowInfo,
|
||||
required this.rowId,
|
||||
required this.viewId,
|
||||
required RowController dataController,
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: rowInfo.viewId),
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||
_dataController = dataController,
|
||||
super(RowState.initial(rowInfo, dataController.loadData())) {
|
||||
super(RowState.initial(dataController.loadData())) {
|
||||
on<RowEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@@ -29,7 +32,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
await _startListening();
|
||||
},
|
||||
createRow: () {
|
||||
_rowBackendSvc.createRow(rowInfo.rowPB.id);
|
||||
_rowBackendSvc.createRowAfterRow(rowId);
|
||||
},
|
||||
didReceiveCells: (cellByFieldId, reason) async {
|
||||
final cells = cellByFieldId.values
|
||||
@@ -78,18 +81,15 @@ class RowEvent with _$RowEvent {
|
||||
@freezed
|
||||
class RowState with _$RowState {
|
||||
const factory RowState({
|
||||
required RowInfo rowInfo,
|
||||
required CellContextByFieldId cellByFieldId,
|
||||
required UnmodifiableListView<GridCellEquatable> cells,
|
||||
RowsChangedReason? changeReason,
|
||||
}) = _RowState;
|
||||
|
||||
factory RowState.initial(
|
||||
RowInfo rowInfo,
|
||||
CellContextByFieldId cellByFieldId,
|
||||
) =>
|
||||
RowState(
|
||||
rowInfo: rowInfo,
|
||||
cellByFieldId: cellByFieldId,
|
||||
cells: UnmodifiableListView(
|
||||
cellByFieldId.values
|
||||
|
||||
+3
-3
@@ -27,7 +27,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
}
|
||||
},
|
||||
didReceiveCellDatas: (cells) {
|
||||
emit(state.copyWith(gridCells: cells));
|
||||
emit(state.copyWith(cells: cells));
|
||||
},
|
||||
deleteField: (fieldId) {
|
||||
_fieldBackendService(fieldId).deleteField();
|
||||
@@ -95,10 +95,10 @@ class RowDetailEvent with _$RowDetailEvent {
|
||||
@freezed
|
||||
class RowDetailState with _$RowDetailState {
|
||||
const factory RowDetailState({
|
||||
required List<DatabaseCellContext> gridCells,
|
||||
required List<DatabaseCellContext> cells,
|
||||
}) = _RowDetailState;
|
||||
|
||||
factory RowDetailState.initial() => RowDetailState(
|
||||
gridCells: List.empty(),
|
||||
cells: List.empty(),
|
||||
);
|
||||
}
|
||||
Loaded 100 of 482 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user