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