Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05c1924940 | ||
|
|
b2f3f902b2 | ||
|
|
33fbba18ca | ||
|
|
cea3c69239 | ||
|
|
ff91c0a909 | ||
|
|
d1efda4c50 | ||
|
|
2b1d1ba2f4 | ||
|
|
9a237b5f18 | ||
|
|
fc21d1d245 | ||
|
|
0c6d4df14b | ||
|
|
8df5d8dcb4 | ||
|
|
aa9f285f59 | ||
|
|
6173022a15 | ||
|
|
afb479607b | ||
|
|
342c361184 | ||
|
|
790d5612f7 | ||
|
|
99a4e330e8 | ||
|
|
c3b702849f | ||
|
|
a49acd9220 | ||
|
|
dd3b4dc5fb | ||
|
|
155817a0f1 | ||
|
|
89c4629ec2 | ||
|
|
6ca81b3a66 | ||
|
|
b278e14ffd | ||
|
|
cb6f7cb4b6 | ||
|
|
f94feb780a | ||
|
|
0ef1b27ae5 | ||
|
|
64994b3336 | ||
|
|
6966b303ff | ||
|
|
ab8e01bbf7 | ||
|
|
e25633636b | ||
|
|
d87a209c4a | ||
|
|
a33cf1f488 | ||
|
|
91236006d4 | ||
|
|
fd9baf7a06 | ||
|
|
ed646ccba2 | ||
|
|
7bc358d7ac | ||
|
|
14fc287071 | ||
|
|
cdf7dced8b | ||
|
|
552c59218c | ||
|
|
c7e0e36902 | ||
|
|
1f82c6682b | ||
|
|
21261ee813 | ||
|
|
15deb8ea79 | ||
|
|
f7f99a162e | ||
|
|
c05f19edd2 | ||
|
|
407e88b27f | ||
|
|
512113877b | ||
|
|
8f7cb50dd4 | ||
|
|
7dedb84504 | ||
|
|
b2ca5c77f6 | ||
|
|
11d720465b | ||
|
|
c2643bfb6c | ||
|
|
3d4d4cf709 | ||
|
|
b965a5f3ae | ||
|
|
73463cd7e3 | ||
|
|
6158954d97 | ||
|
|
3190eebf6e | ||
|
|
dfe994b341 | ||
|
|
92722d0922 | ||
|
|
e04c1bdaec | ||
|
|
20b16cf174 | ||
|
|
5ffa27f545 | ||
|
|
00cdbe5a1c | ||
|
|
dffb865a66 | ||
|
|
5e581e912f | ||
|
|
256d015967 | ||
|
|
d9b3f3f6c6 | ||
|
|
09fa75f5ec | ||
|
|
38c2937f64 | ||
|
|
06d5bc734b | ||
|
|
a521541cb7 | ||
|
|
7d61252e6a | ||
|
|
cb2b9ac537 | ||
|
|
af31f8cf6f | ||
|
|
87c1245a3f | ||
|
|
49d9417cac | ||
|
|
1f29235538 | ||
|
|
fd1e36b21a | ||
|
|
7db11c7cfd | ||
|
|
33888d583b | ||
|
|
3959cdba3a | ||
|
|
200b367e4c | ||
|
|
956d2dfd07 | ||
|
|
83e50d376e | ||
|
|
802a667907 | ||
|
|
e67e9cc647 | ||
|
|
07c4389f6a | ||
|
|
171c5634f2 | ||
|
|
d7bda10e6f | ||
|
|
20bff9003e | ||
|
|
6e0534400b | ||
|
|
17c116a53b | ||
|
|
27b769362b | ||
|
|
b8e7d57ee6 | ||
|
|
ed052c6792 | ||
|
|
5cd2c63320 | ||
|
|
15567a81e9 | ||
|
|
7ab68dcc2c | ||
|
|
30131fd9e4 | ||
|
|
ddcdd545d9 | ||
|
|
d25a399aba | ||
|
|
ee96a44fef | ||
|
|
dda3962249 | ||
|
|
c2743472bc | ||
|
|
e73fd56152 | ||
|
|
04a013f7ee | ||
|
|
b966e3ea07 | ||
|
|
9d53f758d4 | ||
|
|
0689f4e7e1 | ||
|
|
867d515a35 | ||
|
|
381d946808 | ||
|
|
f307300b96 | ||
|
|
d74e7b63ca | ||
|
|
792eec7d4b | ||
|
|
67e93a12e6 | ||
|
|
e188552c1e | ||
|
|
ebb1b6dffb | ||
|
|
25f9bee963 | ||
|
|
6a6fac7f82 | ||
|
|
555b4b48bb | ||
|
|
3522569f97 | ||
|
|
699ea150ce | ||
|
|
e4385adfa9 | ||
|
|
1d46923c47 | ||
|
|
0bf706f438 | ||
|
|
399b7dd682 | ||
|
|
e8f2940024 | ||
|
|
62d5d66d20 | ||
|
|
8b672a159f | ||
|
|
d68212f4ce | ||
|
|
592390dc84 | ||
|
|
d2b2f17b1c | ||
|
|
2c88653a69 | ||
|
|
45b4eb4b3a | ||
|
|
7b93bbe5ff | ||
|
|
d21c0c0dfc | ||
|
|
f5e46967ec | ||
|
|
e0885e2567 | ||
|
|
3b56887267 | ||
|
|
b5d5312c70 | ||
|
|
722b436cad | ||
|
|
bb50466aa9 | ||
|
|
da0395be5e | ||
|
|
9e82f3d7b8 | ||
|
|
67fe0d6bfd | ||
|
|
dddf5aa195 | ||
|
|
92945cafdf | ||
|
|
5cf6617231 | ||
|
|
03c84ff8b5 | ||
|
|
0cf3ade332 | ||
|
|
7c24b6feb0 | ||
|
|
687121ff14 | ||
|
|
e32c584b3b | ||
|
|
64e4416f54 | ||
|
|
ca195887e0 | ||
|
|
f9a5458b94 | ||
|
|
244d072a65 | ||
|
|
d9bc97e012 | ||
|
|
e7491e5182 | ||
|
|
550b8835c6 | ||
|
|
1851721d9a | ||
|
|
603d65a790 | ||
|
|
0cba3f9e3f | ||
|
|
81960a7f05 | ||
|
|
62c4a8c541 | ||
|
|
510d8357ee | ||
|
|
4c6f6f14f3 | ||
|
|
27af57289f | ||
|
|
17aa8c9036 | ||
|
|
c910bda534 | ||
|
|
8d01b96281 | ||
|
|
55f12d5358 | ||
|
|
018c146d72 | ||
|
|
068ac0e992 | ||
|
|
b036129efb | ||
|
|
b3c8eb151a | ||
|
|
e86d584ea7 | ||
|
|
e0226e54a5 | ||
|
|
bde1457524 | ||
|
|
2ad2a79bd0 | ||
|
|
f013bb9d6e | ||
|
|
1b4a723500 | ||
|
|
b5d2af3371 | ||
|
|
4205a34f04 | ||
|
|
e86a9d697c | ||
|
|
f82dabcc75 | ||
|
|
09717d92c5 |
No files matched your search
@@ -7,7 +7,6 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -20,7 +19,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
@@ -152,7 +151,7 @@ jobs:
|
||||
rustup target install aarch64-linux-android
|
||||
rustup target install x86_64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked duckscript_cli
|
||||
cargo install cargo-ndk
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
|
||||
@@ -2,9 +2,9 @@ name: Docker-CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "release/*"]
|
||||
branches: [ "main", "release/*" ]
|
||||
pull_request:
|
||||
branches: ["main", "release/*"]
|
||||
branches: [ "main", "release/*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [windows-latest]
|
||||
os: [ windows-latest ]
|
||||
include:
|
||||
- os: windows-latest
|
||||
flutter_profile: development-windows-x86
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [ macos-latest ]
|
||||
include:
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac-x86_64
|
||||
@@ -123,12 +123,12 @@ jobs:
|
||||
flutter_profile: ${{ matrix.flutter_profile }}
|
||||
|
||||
unit_test:
|
||||
needs: [prepare-linux]
|
||||
needs: [ prepare-linux ]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -217,11 +217,11 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
cloud_integration_test:
|
||||
needs: [prepare-linux]
|
||||
needs: [ prepare-linux ]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -340,13 +340,13 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
integration_test:
|
||||
needs: [prepare-linux]
|
||||
needs: [ prepare-linux ]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
os: [ ubuntu-latest ]
|
||||
test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
pull_request:
|
||||
@@ -16,12 +15,11 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -85,7 +83,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
rustup target install aarch64-apple-ios-sim
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked duckscript_cli
|
||||
cargo install cargo-lipo
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -73,8 +73,8 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
vcpkg integrate install
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- name: Build Windows app
|
||||
working-directory: frontend
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
|
||||
- { target: x86_64-apple-darwin, os: macos-13, extra-build-args: "" }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -158,8 +158,8 @@ jobs:
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
@@ -232,10 +232,10 @@ jobs:
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -256,8 +256,8 @@ jobs:
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
@@ -336,12 +336,12 @@ jobs:
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
flutter_profile: production-linux-x86_64,
|
||||
}
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
flutter_profile: production-linux-x86_64,
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -370,8 +370,8 @@ jobs:
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install -y alien libnotify-dev
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
rustup target add ${{ matrix.job.target }}
|
||||
|
||||
- name: Install gcc-aarch64-linux-gnu
|
||||
|
||||
@@ -18,81 +18,11 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CLOUD_VERSION: 0.7.6-amd64
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
CLOUD_VERSION: 0.8.3-amd64
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
self-hosted-job:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Appflowy Cloud
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: AppFlowy-IO/AppFlowy-Cloud
|
||||
path: AppFlowy-Cloud
|
||||
|
||||
- name: Prepare Appflowy Cloud env
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
cp deploy.env .env
|
||||
sed -i '' 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||
sed -i '' 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
|
||||
sed -i '' 's|APPFLOWY_AI_OPENAI_API_KEY=.*|APPFLOWY_AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}|' .env
|
||||
|
||||
- name: Ensure AppFlowy-Cloud is Running with Correct Version
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
else
|
||||
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
|
||||
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
|
||||
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
echo "Waiting for the container to be ready..."
|
||||
sleep 10
|
||||
docker ps -a
|
||||
docker compose logs
|
||||
else
|
||||
echo "AppFlowy-Cloud is running with the correct version."
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Run rust-lib tests
|
||||
working-directory: frontend/rust-lib
|
||||
env:
|
||||
RUST_LOG: info
|
||||
RUST_BACKTRACE: 1
|
||||
af_cloud_test_base_url: http://localhost
|
||||
af_cloud_test_ws_url: ws://localhost/ws/v1
|
||||
af_cloud_test_gotrue_url: http://localhost/gotrue
|
||||
run: |
|
||||
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
|
||||
|
||||
- name: rustfmt rust-lib
|
||||
run: cargo fmt --all -- --check
|
||||
working-directory: frontend/rust-lib/
|
||||
|
||||
- name: clippy rust-lib
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
working-directory: frontend/rust-lib
|
||||
|
||||
ubuntu-job:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set timezone for action
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -40,8 +40,8 @@ jobs:
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
name: Tauri-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.77.2"
|
||||
CARGO_MAKE_VERSION: "0.36.6"
|
||||
CI: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# tauri-build-self-hosted:
|
||||
# if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
# runs-on: self-hosted
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - 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"
|
||||
|
||||
tauri-build-ubuntu:
|
||||
#if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space
|
||||
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: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_web_app/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies
|
||||
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
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
- name: install tauri deps tools
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
shell: bash
|
||||
|
||||
- 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"
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Tauri-CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build/tauri
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
|
||||
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_tauri/src-tauri -> target"
|
||||
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_tauri/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_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- name: frontend tests and linting
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
pnpm test
|
||||
pnpm test:errors
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
args: "--debug"
|
||||
@@ -1,153 +0,0 @@
|
||||
name: Publish Tauri Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'The branch to release'
|
||||
required: true
|
||||
default: 'main'
|
||||
version:
|
||||
description: 'The version to release'
|
||||
required: true
|
||||
default: '0.0.0'
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
|
||||
jobs:
|
||||
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- platform: windows-latest
|
||||
args: "--verbose"
|
||||
target: "windows-x86_64"
|
||||
- platform: macos-latest
|
||||
args: "--target x86_64-apple-darwin"
|
||||
target: "macos-x86_64"
|
||||
- platform: ubuntu-20.04
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
target: "linux-x86_64"
|
||||
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.settings.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_tauri/src-tauri -> target"
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force duckscript_cli
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.settings.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_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }}
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- uses: tauri-apps/tauri-action@dev
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }}
|
||||
APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }}
|
||||
CI: true
|
||||
with:
|
||||
args: ${{ matrix.settings.args }}
|
||||
appVersion: ${{ github.event.inputs.version }}
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
|
||||
- name: Upload EXE package(windows only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.exe
|
||||
path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe
|
||||
|
||||
- name: Upload DMG package(macos only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'macos-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.dmg
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg
|
||||
|
||||
- name: Upload Deb package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.deb
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
||||
|
||||
- name: Upload AppImage package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
||||
@@ -1,75 +0,0 @@
|
||||
name: Web-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-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
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: 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: Run lint check
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint
|
||||
|
||||
- name: build and analyze
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run analyze >> analyze-size.txt
|
||||
- name: Upload analyze-size.txt
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: analyze-size.txt
|
||||
path: frontend/appflowy_web_app/analyze-size.txt
|
||||
retention-days: 30
|
||||
- name: Upload stats.html
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stats.html
|
||||
path: frontend/appflowy_web_app/dist/stats.html
|
||||
retention-days: 30
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Web Code Coverage
|
||||
|
||||
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:
|
||||
test:
|
||||
if: github.event.pull_request.draft != true
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space (ubuntu only)
|
||||
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 }}
|
||||
# Install pnpm dependencies, cache them correctly
|
||||
# and run all Cypress tests
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
working-directory: frontend/appflowy_web_app
|
||||
component: true
|
||||
build: pnpm run build
|
||||
start: pnpm run start
|
||||
browser: chrome
|
||||
|
||||
- name: Jest run
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run test:unit
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: cf9245e0-e136-4e21-b0ee-35755fa0c493
|
||||
files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info
|
||||
flags: appflowy_web_app
|
||||
name: frontend/appflowy_web_app
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
+97
-2
@@ -1,5 +1,100 @@
|
||||
# Release Notes
|
||||
# Release Notes
|
||||
## Version 0.8.1 - 14/01/2025
|
||||
### New Features:
|
||||
- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only
|
||||
- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat
|
||||
- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language
|
||||
- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more
|
||||
- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar
|
||||
- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile
|
||||
### Bug Fixes
|
||||
- Resolved an icon rendering issue in callout blocks, tab bars, and search results
|
||||
- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails
|
||||
|
||||
## Version 0.8.0 - 06/01/2025
|
||||
### Bug Fixes
|
||||
- Fixed error displaying in the page style menu
|
||||
- Fixed filter logic in the icon picker
|
||||
- Fixed error displaying in the Favorite/Recent page
|
||||
- Fixed the color picker displaying when tapping down
|
||||
- Fixed icons not being supported in subpage blocks
|
||||
- Fixed recent icon functionality in the space icon menu
|
||||
- Fixed "Insert Below" not auto-scrolling the table
|
||||
- Fixed a to-do item with an emoji automatically creating a soft break
|
||||
- Fixed header row/column tap areas being too small
|
||||
- Fixed simple table alignment not working for items that wrap
|
||||
- Fixed web content reverting after removing the inline code format on desktop
|
||||
- Fixed inability to make changes to a row or column in the table when opening a new tab
|
||||
- Fixed changing the language to CKB-KU causing a gray screen on mobile
|
||||
|
||||
## Version 0.7.9 - 30/12/2024
|
||||
### New Features
|
||||
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
|
||||
- Create beautiful documents with 22 content types and markdown support
|
||||
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
|
||||
- Invite members to your workspace for seamless collaboration
|
||||
- Create multiple public/private spaces to better organize your content
|
||||
- Simple Table is now available on Mobile, designed specifically for mobile devices.
|
||||
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
|
||||
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
|
||||
- Use '/' to insert a content block into a table cell on Desktop
|
||||
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
|
||||
- Add messages to an editable document while chatting with AI side by side
|
||||
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
|
||||
- Drag a page from the sidebar into a document to easily mention the page without typing its title
|
||||
- Paste as plain text, a new option in the right-click paste menu
|
||||
### Bug Fixes
|
||||
- Fixed misalignment in numbered lists
|
||||
- Resolved several bugs in the emoji menu
|
||||
- Fixed a bug with checklist items
|
||||
|
||||
## Version 0.7.8 - 18/12/2024
|
||||
### New Features
|
||||
<img width="1068" alt="image" src="https://github.com/user-attachments/assets/cf8bd287-f370-4291-8638-76e2bbf4aaac" />
|
||||
|
||||
- Meet Simple Table 2.0:
|
||||
- Insert a list into a table cell
|
||||
- Insert images, quotes, callouts, and code blocks into a table cell
|
||||
- Drag to move rows or columns
|
||||
- Toggle header rows or columns on/off
|
||||
- Distribute columns evenly
|
||||
- Adjust to page width
|
||||
- Enjoy a new UI/UX for a seamless experience
|
||||
- Revamped mention page interactions in AI Chat
|
||||
- Improved AppFlowy AI service
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an error when opening files in the database in local mode
|
||||
- Fixed arrow up/down navigation not working for selecting a language in Code Block
|
||||
- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work
|
||||
|
||||
## Version 0.7.7 - 09/12/2024
|
||||
### Bug Fixes
|
||||
- Fixed sidebar menu resize regression
|
||||
- Fixed AI chat loading issues
|
||||
- Fixed inability to open local files in database
|
||||
- Fixed mentions remaining in notifications after removal from document
|
||||
- Fixed event card closing when clicking on empty space
|
||||
- Fixed keyboard shortcut issues
|
||||
|
||||
## Version 0.7.6 - 03/12/2024
|
||||
### New Features
|
||||
- Revamped the simple table UI
|
||||
- Added support for capturing images from camera on mobile
|
||||
### Bug Fixes
|
||||
- Improved markdown rendering capabilities in AI writer
|
||||
- Fixed an issue where pressing Enter on a collapsed toggle list would add an unnecessary new line
|
||||
- Fixed an issue where creating a document from slash menu could insert content at incorrect position
|
||||
|
||||
## Version 0.7.5 - 25/11/2024
|
||||
### Bug Fixes
|
||||
- Improved chat response parsing
|
||||
- Fixed toggle list icon direction for RTL mode
|
||||
- Fixed cross blocks formatting not reflecting in float toolbar
|
||||
- Fixed unable to click inside the toggle list to create a new paragraph
|
||||
- Fixed open file error 50 on macOS
|
||||
- Fixed upload file exceed limit error
|
||||
|
||||
## Version 0.7.4 - 19/11/2024
|
||||
### New Features
|
||||
- Support uploading WebP and BMP images
|
||||
@@ -931,4 +1026,4 @@ Bug fixes and improvements
|
||||
- Increased height of action
|
||||
- CPU performance issue
|
||||
- Fix potential data parser error
|
||||
- More foundation work for online collaboration
|
||||
- More foundation work for online collaboration
|
||||
@@ -42,10 +42,12 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
## User Installation
|
||||
|
||||
- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases)
|
||||
- Other channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
|
||||
- Other
|
||||
channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
|
||||
- Available on
|
||||
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
|
||||
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is not supported
|
||||
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
|
||||
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is
|
||||
not supported
|
||||
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
|
||||
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
|
||||
|
||||
@@ -61,15 +63,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
|
||||
## Getting Started with development
|
||||
|
||||
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
|
||||
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific
|
||||
development instructions
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
|
||||
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
|
||||
|
||||
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
|
||||
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
|
||||
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+)
|
||||
|
||||
## **Releases**
|
||||
|
||||
@@ -77,16 +82,24 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/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.
|
||||
|
||||
## Translations 🌎🗺
|
||||
|
||||
[](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
|
||||
|
||||
@@ -96,16 +109,30 @@ To add translations, you can manually edit the JSON translation files in `/front
|
||||
|
||||
## Why Are We Building This?
|
||||
|
||||
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
|
||||
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and
|
||||
functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations.
|
||||
These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative
|
||||
workplace management tools also have their constraints.
|
||||
|
||||
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market.
|
||||
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have
|
||||
led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates
|
||||
from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a
|
||||
proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to
|
||||
come up with a one-size fits all solution in such a fragmented market.
|
||||
|
||||
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well.
|
||||
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up,
|
||||
in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is
|
||||
a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention
|
||||
the speed and native experience. The same may apply to individual users as well.
|
||||
|
||||
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 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:
|
||||
|
||||
@@ -113,16 +140,20 @@ We decided to achieve this mission by upholding the three most fundamental value
|
||||
- Reliable native experience
|
||||
- Community-driven extensibility
|
||||
|
||||
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information.
|
||||
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for
|
||||
more information.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to these amazing projects which help power AppFlowy.IO:
|
||||
|
||||
- [flutter-quill](https://github.com/singerdmx/flutter-quill)
|
||||
- [cargo-make](https://github.com/sagiegurari/cargo-make)
|
||||
- [contrib.rocks](https://contrib.rocks)
|
||||
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
|
||||
+1
-1
@@ -20,7 +20,7 @@ workflows:
|
||||
|
||||
rustup target install aarch64-apple-ios-sim
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo install --force --locked duckscript_cli
|
||||
cargo install --force cargo-lipo
|
||||
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
|
||||
Vendored
-15
@@ -121,20 +121,5 @@
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
{
|
||||
// https://tauri.app/v1/guides/debugging/vs-code
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "AF-tauri: Debug backend",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml",
|
||||
"--no-default-features"
|
||||
]
|
||||
},
|
||||
"preLaunchTask": "AF: Tauri UI Dev",
|
||||
"cwd": "${workspaceRoot}/appflowy_tauri/"
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
-45
@@ -245,51 +245,6 @@
|
||||
"problemMatcher": [],
|
||||
"detail": "appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri UI Build",
|
||||
"type": "shell",
|
||||
"command": "pnpm run build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri UI Dev",
|
||||
"type": "shell",
|
||||
"isBackground": true,
|
||||
"command": "pnpm sync:i18n && pnpm run dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri Clean",
|
||||
"type": "shell",
|
||||
"command": "cargo make tauri_clean",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri Clean + Dev",
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Tauri Clean",
|
||||
"AF: Tauri UI Dev"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri ESLint",
|
||||
"type": "shell",
|
||||
"command": "npx eslint --fix src",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Env File",
|
||||
"type": "shell",
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.7.4"
|
||||
APPFLOWY_VERSION = "0.8.1"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -53,7 +53,7 @@ android {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="appflowy-flutter" />
|
||||
<!-- <data android:host="login-callback" /> -->
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!--
|
||||
@@ -50,7 +49,8 @@
|
||||
<!-- Permission to read files from external storage (outside application container).
|
||||
As of Android 12 this permission no longer has any effect. Instead use the
|
||||
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READM_MEDIA_AUDIO permissions. -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<!-- Permissions to read media files. -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<queries>
|
||||
@@ -65,4 +65,5 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
</manifest>
|
||||
@@ -1,16 +1,20 @@
|
||||
import 'data_migration/data_migration_test_runner.dart'
|
||||
as data_migration_test_runner;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'set_env.dart' as preset_af_cloud_env_test;
|
||||
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
|
||||
import 'sidebar/sidebar_rename_untitled_test.dart'
|
||||
as sidebar_rename_untitled_test;
|
||||
import 'uncategorized/uncategorized_test_runner.dart'
|
||||
as uncategorized_test_runner;
|
||||
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
|
||||
import 'data_migration/data_migration_test_runner.dart'
|
||||
as data_migration_test_runner;
|
||||
import 'set_env.dart' as preset_af_cloud_env_test;
|
||||
|
||||
Future<void> main() async {
|
||||
preset_af_cloud_env_test.main();
|
||||
|
||||
data_migration_test_runner.main();
|
||||
|
||||
// uncategorized
|
||||
uncategorized_test_runner.main();
|
||||
|
||||
@@ -22,4 +26,6 @@ Future<void> main() async {
|
||||
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('AI Writer:', () {
|
||||
testWidgets('the ai writer transaction should only apply in memory',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_aiWriter.tr(),
|
||||
);
|
||||
expect(find.byType(AIWriterBlockComponent), findsOneWidget);
|
||||
|
||||
// switch to another page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
// switch back to the page
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// expect the ai writer block is not in the document
|
||||
expect(find.byType(AIWriterBlockComponent), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
@@ -1,5 +1,6 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_ai_writer_test.dart' as document_ai_writer_test;
|
||||
import 'document_copy_link_to_block_test.dart'
|
||||
as document_copy_link_to_block_test;
|
||||
import 'document_option_actions_test.dart' as document_option_actions_test;
|
||||
@@ -11,4 +12,5 @@ void main() {
|
||||
document_option_actions_test.main();
|
||||
document_copy_link_to_block_test.main();
|
||||
document_publish_test.main();
|
||||
document_ai_writer_test.main();
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('Change slide bar space icon', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(SidebarSpaceHeader),
|
||||
onHover: () async {
|
||||
final moreOption = find.byType(SpaceMorePopup);
|
||||
await tester.tapButton(moreOption);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsNothing);
|
||||
await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final icons = find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
);
|
||||
expect(icons, findsOneWidget);
|
||||
await tester.tapIcon(EmojiIconData.icon(firstIcon));
|
||||
|
||||
final spaceHeader = find.byType(SidebarSpaceHeader);
|
||||
final spaceIcon = find.descendant(
|
||||
of: spaceHeader,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
),
|
||||
);
|
||||
expect(spaceIcon, findsOneWidget);
|
||||
});
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('Rename empty name view (untitled)', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// click the ... button and open rename dialog
|
||||
await tester.hoverOnPageName(
|
||||
ViewLayoutPB.Document.defaultName,
|
||||
onHover: () async {
|
||||
await tester.tapPageOptionButton();
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.disclosureAction_rename.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(NavigatorTextFieldDialog), findsOneWidget);
|
||||
|
||||
final textField = tester.widget<FlowyFormTextInput>(
|
||||
find.descendant(
|
||||
of: find.byType(NavigatorTextFieldDialog),
|
||||
matching: find.byType(FlowyFormTextInput),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
textField.controller!.text,
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
});
|
||||
}
|
||||
+67
-7
@@ -1,10 +1,11 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/loading.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_menu.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -102,8 +103,7 @@ void main() {
|
||||
expect(memberCount, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('only display one menu item in the workspace menu',
|
||||
(tester) async {
|
||||
testWidgets('workspace menu popover behavior test', (tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
@@ -128,6 +128,8 @@ void main() {
|
||||
final workspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
|
||||
);
|
||||
|
||||
// the workspace menu shouldn't conflict with logout
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
@@ -136,15 +138,73 @@ void main() {
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
final logoutButton = find.byType(WorkspaceMoreButton);
|
||||
await tester.tapButton(logoutButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
|
||||
expect(moreButton, findsNothing);
|
||||
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
|
||||
expect(moreButton, findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more action button for the same workspace shouldn't do
|
||||
// anything
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
// click it again
|
||||
await tester.tapButton(moreButton);
|
||||
|
||||
// nothing should happen
|
||||
expect(
|
||||
find.text(LocaleKeys.button_rename.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more button of another workspace should close the menu
|
||||
// for this one
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final otherWorspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
|
||||
);
|
||||
final otherMoreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
otherWorspaceItem,
|
||||
onHover: () async {
|
||||
expect(otherMoreButton, findsOneWidget);
|
||||
await tester.tapButton(otherMoreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
expect(moreButton, findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ void main() {
|
||||
);
|
||||
|
||||
final shareValues = plainText!
|
||||
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
|
||||
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
|
||||
.split('/');
|
||||
final workspaceId = shareValues[0];
|
||||
expect(workspaceId, isNotEmpty);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
|
||||
@@ -71,5 +72,16 @@ void main() {
|
||||
|
||||
expect(find.byType(FlowyTab), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the space view should not be opened', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
expect(find.byType(AppFlowyEditorPage), findsNothing);
|
||||
expect(find.text('Blank page'), findsOne);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -249,7 +249,7 @@ More actions for published page:
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.sites);
|
||||
// wait the backend return the sites data
|
||||
await tester.wait(1000);
|
||||
await tester.wait(2000);
|
||||
|
||||
// check if the page is published in sites page
|
||||
final pageItem = find.byWidgetPredicate(
|
||||
|
||||
+99
-3
@@ -1,6 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -8,7 +17,9 @@ import 'package:integration_test/integration_test.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('Folder Search', () {
|
||||
testWidgets('Search for views', (tester) async {
|
||||
@@ -45,9 +56,94 @@ void main() {
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
// The score should be higher for "ViewOne" thus it should be shown first
|
||||
final firstDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
final firstDocumentWidget = tester.widget(
|
||||
find.byType(SearchResultTile).first,
|
||||
) as SearchResultTile;
|
||||
expect(firstDocumentWidget.result.data, firstDocument);
|
||||
});
|
||||
|
||||
testWidgets('Displaying icons in search results', (tester) async {
|
||||
final randomValue = Random().nextInt(10000) + 10000;
|
||||
final pageNames = ['First Page-$randomValue', 'Second Page-$randomValue'];
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
|
||||
/// create two pages
|
||||
for (final pageName in pageNames) {
|
||||
await tester.createNewPageWithNameUnderParent(name: pageName);
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: pageName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
icon: emojiIconData,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.toggleCommandPalette();
|
||||
|
||||
/// search for `Page`
|
||||
final searchFieldFinder = find.descendant(
|
||||
of: find.byType(SearchField),
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
await tester.enterText(searchFieldFinder, 'Page-$randomValue');
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
expect(find.byType(SearchResultTile), findsNWidgets(2));
|
||||
|
||||
/// check results
|
||||
final svgs = find.descendant(
|
||||
of: find.byType(SearchResultTile),
|
||||
matching: find.byType(FlowySvg),
|
||||
);
|
||||
expect(svgs, findsNWidgets(2));
|
||||
|
||||
final firstSvg = svgs.first.evaluate().first.widget as FlowySvg,
|
||||
lastSvg = svgs.last.evaluate().first.widget as FlowySvg;
|
||||
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
|
||||
/// icon displayed correctly
|
||||
expect(firstSvg.svgString, iconData.svgString);
|
||||
expect(lastSvg.svgString, iconData.svgString);
|
||||
|
||||
testWidgets('select the content in document and search', (tester) async {
|
||||
const firstDocument = ''; // empty document
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(name: firstDocument);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(FloatingToolbar),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.toggleCommandPalette();
|
||||
expect(find.byType(CommandPaletteModal), findsOneWidget);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(firstDocument),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+11
-1
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
@@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
@@ -301,6 +309,7 @@ void main() {
|
||||
await tester.createOption(name: "qwer");
|
||||
await tester.selectOption(name: "asdf");
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, "Tags");
|
||||
@@ -332,6 +341,7 @@ void main() {
|
||||
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
|
||||
await tester.selectOption(name: "asdf");
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(0);
|
||||
|
||||
|
||||
+11
-4
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid edit field test:', () {
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
@@ -538,8 +545,8 @@ void main() {
|
||||
|
||||
// edit the first date cell
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
|
||||
await tester.toggleIncludeTime();
|
||||
final now = DateTime.now();
|
||||
await tester.toggleIncludeTime();
|
||||
await tester.selectDay(content: now.day);
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
-57
@@ -4,12 +4,10 @@ import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/af_image.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -26,61 +24,6 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database row cover', () {
|
||||
testWidgets('add image to media field and check if cover is set (grid)',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Invoke the field editor
|
||||
await tester.tapGridFieldWithName('Type');
|
||||
await tester.tapEditFieldButton();
|
||||
|
||||
// Change to media type
|
||||
await tester.tapSwitchFieldTypeButton();
|
||||
await tester.selectFieldType(FieldType.Media);
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// Prepare file for upload from local
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
|
||||
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final file = File(imagePath)
|
||||
..writeAsBytesSync(image.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(paths: [imagePath]);
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
|
||||
|
||||
// Open media cell editor
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
|
||||
await tester.findMediaCellEditor(findsOneWidget);
|
||||
|
||||
// Click on add file button in the Media Cell Editor
|
||||
await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the upload interaction
|
||||
await tester.tapFileUploadHint();
|
||||
|
||||
// Expect one file
|
||||
expect(find.byType(RenderMedia), findsOneWidget);
|
||||
|
||||
// Close cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Open first row in row detail view
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect a cover to be shown
|
||||
expect(find.byType(RowCover), findsOneWidget);
|
||||
|
||||
// Remove the temp file
|
||||
await Future.wait([file.delete()]);
|
||||
});
|
||||
|
||||
testWidgets('add and remove cover from Row Detail Card', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
+17
-3
@@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_edi
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -21,7 +22,14 @@ import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid row detail page:', () {
|
||||
testWidgets('opens', (tester) async {
|
||||
@@ -386,11 +394,16 @@ void main() {
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 1);
|
||||
tester.assertPhantomChecklistItemContent("");
|
||||
|
||||
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(ChecklistRowDetailCell),
|
||||
onHover: () async {
|
||||
await tester.tapButton(find.byType(ChecklistItemControl));
|
||||
},
|
||||
);
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
@@ -398,6 +411,7 @@ void main() {
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 2);
|
||||
tester.assertPhantomChecklistItemContent("");
|
||||
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('callout with emoji icon picker', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
|
||||
/// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
/// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
/// create callout
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(),
|
||||
);
|
||||
|
||||
/// select an icon
|
||||
final emojiPickerButton = find.descendant(
|
||||
of: find.byType(CalloutBlockComponentWidget),
|
||||
matching: find.byType(EmojiPickerButton),
|
||||
);
|
||||
await tester.tapButton(emojiPickerButton);
|
||||
await tester.tapIcon(emojiIconData);
|
||||
|
||||
/// verification results
|
||||
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
final iconWidget = find
|
||||
.descendant(
|
||||
of: emojiPickerButton,
|
||||
matching: find.byType(IconWidget),
|
||||
)
|
||||
.evaluate()
|
||||
.first
|
||||
.widget as IconWidget;
|
||||
final iconWidgetData = iconWidget.iconsData;
|
||||
expect(iconWidgetData.svgString, iconData.svgString);
|
||||
expect(iconWidgetData.iconName, iconData.iconName);
|
||||
expect(iconWidgetData.groupName, iconData.groupName);
|
||||
});
|
||||
}
|
||||
+46
-4
@@ -1,7 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
@@ -11,6 +9,7 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
@@ -27,10 +26,13 @@ void main() {
|
||||
await tester.pasteContent(
|
||||
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 3);
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final text =
|
||||
editorState.document.root.children.first.delta!.toPlainText();
|
||||
final textLines = text.split('\n');
|
||||
for (var i = 0; i < lines; i++) {
|
||||
expect(
|
||||
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
|
||||
textLines[i],
|
||||
'line $i',
|
||||
);
|
||||
}
|
||||
@@ -467,6 +469,44 @@ void main() {
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
# I'm h1
|
||||
## I'm h2
|
||||
### I'm h3
|
||||
#### I'm h4
|
||||
##### I'm h5
|
||||
###### I'm h6''';
|
||||
|
||||
testWidgets('paste markdowns', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
(editorState) {
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
expect(text, 'I\'m h$i');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste markdowns as plain', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
pasteAsPlain: true,
|
||||
(editorState) {
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
final expectText = '${'#' * i} I\'m h$i';
|
||||
expect(text, expectText);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
@@ -476,6 +516,7 @@ extension on WidgetTester {
|
||||
String? plainText,
|
||||
String? html,
|
||||
String? inAppJson,
|
||||
bool pasteAsPlain = false,
|
||||
(String, Uint8List?)? image,
|
||||
}) async {
|
||||
await initializeAppFlowy();
|
||||
@@ -502,6 +543,7 @@ extension on WidgetTester {
|
||||
await simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isShiftPressed: pasteAsPlain,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await pumpAndSettle();
|
||||
|
||||
+22
-6
@@ -37,12 +37,12 @@ void main() {
|
||||
|
||||
// set clipboard data
|
||||
final data = [
|
||||
"123456\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"1234567\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"12345678\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"123456\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"1234567\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"12345678\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
].join();
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
@@ -139,6 +139,22 @@ void main() {
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
/// press cmd/ctrl+F to display the find menu
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyF,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget);
|
||||
|
||||
/// press esc to dismiss the find menu
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsNothing);
|
||||
},
|
||||
);
|
||||
}
|
||||
+19
-2
@@ -2,12 +2,12 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -330,6 +330,23 @@ void main() {
|
||||
expect(find.text("$_createdPageName (copy)"), findsNWidgets(2));
|
||||
expect(find.text("$_createdPageName (copy) (copy)"), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Cancel inline page reference menu by space', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showPlusMenu();
|
||||
|
||||
// Cancel by space
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.space,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(InlineActionsMenu), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+106
@@ -1,4 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -31,4 +37,104 @@ void main() {
|
||||
expect(pageFinder, findsNWidgets(1));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('count title towards word count', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
Finder title = tester.editor.findDocumentTitle('');
|
||||
|
||||
await tester.openMoreViewActions();
|
||||
final viewMetaInfo = find.byType(ViewMetaInfo);
|
||||
expect(viewMetaInfo, findsOneWidget);
|
||||
|
||||
ViewMetaInfo viewMetaInfoWidget =
|
||||
viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
Counters titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
|
||||
expect(titleCounter.charCount, 0);
|
||||
expect(titleCounter.wordCount, 0);
|
||||
|
||||
/// input [str1] within title
|
||||
const str1 = 'Hello',
|
||||
str2 = '$str1 AppFlowy',
|
||||
str3 = '$str2!',
|
||||
str4 = 'Hello world';
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str1);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str1.length);
|
||||
expect(titleCounter.wordCount, 1);
|
||||
|
||||
/// input [str2] within title
|
||||
title = tester.editor.findDocumentTitle(str1);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str2);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str2.length);
|
||||
expect(titleCounter.wordCount, 2);
|
||||
|
||||
/// input [str3] within title
|
||||
title = tester.editor.findDocumentTitle(str2);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str3);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str3.length);
|
||||
expect(titleCounter.wordCount, 2);
|
||||
|
||||
/// input [str4] within document
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.editor
|
||||
.updateSelection(Selection.collapsed(Position(path: [0])));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor
|
||||
.getCurrentEditorState()
|
||||
.insertTextAtCurrentSelection(str4);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
final texts =
|
||||
find.descendant(of: viewMetaInfo, matching: find.byType(FlowyText));
|
||||
expect(texts, findsNWidgets(3));
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
final Counters documentCounters = viewMetaInfoWidget.documentCounters!;
|
||||
final wordCounter = texts.evaluate().elementAt(0).widget as FlowyText,
|
||||
charCounter = texts.evaluate().elementAt(1).widget as FlowyText;
|
||||
final numberFormat = NumberFormat();
|
||||
expect(
|
||||
wordCounter.text,
|
||||
LocaleKeys.moreAction_wordCount.tr(
|
||||
args: [
|
||||
numberFormat
|
||||
.format(titleCounter.wordCount + documentCounters.wordCount)
|
||||
.toString(),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
charCounter.text,
|
||||
LocaleKeys.moreAction_charCount.tr(
|
||||
args: [
|
||||
numberFormat
|
||||
.format(
|
||||
titleCounter.charCount + documentCounters.charCount,
|
||||
)
|
||||
.toString(),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
+48
-58
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -11,6 +13,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
// Test cases for the Document SubPageBlock that needs to be covered:
|
||||
@@ -37,7 +40,14 @@ import '../../shared/util.dart';
|
||||
const _defaultPageName = "";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Document SubPageBlock tests', () {
|
||||
testWidgets('Insert a new SubPageBlock from Slash menu items',
|
||||
@@ -48,11 +58,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsNWidgets(3),
|
||||
@@ -67,12 +72,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -91,11 +90,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -144,11 +138,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -202,11 +191,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -243,11 +227,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -293,11 +272,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -336,11 +310,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -384,11 +353,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
@@ -411,12 +375,6 @@ void main() {
|
||||
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -437,11 +395,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
|
||||
final beforeNode = tester.editor.getNodeAtPath([1]);
|
||||
@@ -498,6 +451,43 @@ void main() {
|
||||
|
||||
expect(find.text('Parent'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('Displaying icon of subpage', (tester) async {
|
||||
const firstPage = 'FirstPage';
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: firstPage);
|
||||
final icon = await tester.loadIcon();
|
||||
|
||||
/// create subpage
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_subPage_name.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
|
||||
/// add icon
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.tapIcon(icon);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.openPage(firstPage);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: firstPage,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
/// check if there is a icon in document
|
||||
final iconWidget = find.byWidgetPredicate((w) {
|
||||
if (w is! RawEmojiIconWidget) return false;
|
||||
final iconData = w.emoji.emoji;
|
||||
return iconData == icon.emoji;
|
||||
});
|
||||
expect(iconWidget, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -11,6 +11,8 @@ import 'document_with_file_test.dart' as document_with_file_test;
|
||||
import 'document_with_image_block_test.dart' as document_with_image_block_test;
|
||||
import 'document_with_multi_image_block_test.dart'
|
||||
as document_with_multi_image_block_test;
|
||||
import 'document_with_simple_table_test.dart'
|
||||
as document_with_simple_table_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -25,4 +27,5 @@ void main() {
|
||||
document_block_option_test.main();
|
||||
document_find_menu_test.main();
|
||||
document_toolbar_test.main();
|
||||
document_with_simple_table_test.main();
|
||||
}
|
||||
+72
-2
@@ -1,17 +1,34 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('cover image:', () {
|
||||
testWidgets('document cover tests', (tester) async {
|
||||
@@ -51,6 +68,59 @@ void main() {
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
});
|
||||
|
||||
testWidgets('document cover local image tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
|
||||
// Insert a document cover
|
||||
await tester.editor.tapOnAddCover();
|
||||
tester.expectToSeeDocumentCover(CoverType.asset);
|
||||
|
||||
// Hover over the cover to show the 'Change Cover' and delete buttons
|
||||
await tester.editor.hoverOnCover();
|
||||
tester.expectChangeCoverAndDeleteButton();
|
||||
|
||||
// Change cover to a local image image
|
||||
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final imageFile = File(localImagePath)
|
||||
..writeAsBytesSync(imagePath.buffer.asUint8List());
|
||||
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnChangeCover();
|
||||
|
||||
final uploadButton = find.findTextInFlowyText(
|
||||
LocaleKeys.document_imageBlock_upload_label.tr(),
|
||||
);
|
||||
await tester.tapButton(uploadButton);
|
||||
|
||||
mockPickFilePaths(paths: [localImagePath]);
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeDocumentCover(CoverType.file);
|
||||
|
||||
// Remove the cover
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnRemoveCover();
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Test if deleteImageFromLocalStorage(localImagePath) function is called once
|
||||
await tester.pump(kDoubleTapTimeout);
|
||||
expect(deleteImageTestCounter, 1);
|
||||
|
||||
// delete temp files
|
||||
await imageFile.delete();
|
||||
});
|
||||
|
||||
testWidgets('document icon tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -147,7 +217,7 @@ void main() {
|
||||
tester.expectViewHasIcon(
|
||||
gettingStarted,
|
||||
ViewLayoutPB.Document,
|
||||
punch,
|
||||
EmojiIconData.emoji(punch),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+104
@@ -1,12 +1,15 @@
|
||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -174,9 +177,110 @@ void main() {
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('insert a referenced grid with many rows (load more option)',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
|
||||
|
||||
// validate the referenced grid is inserted
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(GridPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3533
|
||||
// test: the selection of editor should be clear when editing the grid
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(
|
||||
Position(path: [1]),
|
||||
),
|
||||
);
|
||||
final gridTextCell = find.byType(EditableTextCell).first;
|
||||
await tester.tapButton(gridTextCell);
|
||||
|
||||
expect(tester.editor.getCurrentEditorState().selection, isNull);
|
||||
|
||||
final editorScrollable = find
|
||||
.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is Scrollable && w.axis == Axis.vertical,
|
||||
),
|
||||
)
|
||||
.first;
|
||||
|
||||
// Add 100 Rows to the linked database
|
||||
final addRowFinder = find.byType(GridAddRowButton);
|
||||
for (var i = 0; i < 100; i++) {
|
||||
await tester.scrollUntilVisible(
|
||||
addRowFinder,
|
||||
100,
|
||||
scrollable: editorScrollable,
|
||||
);
|
||||
await tester.tapButton(addRowFinder);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// Since all rows visible are those we added, we should see all of them
|
||||
expect(find.byType(GridRow), findsNWidgets(103));
|
||||
|
||||
// Navigate to getting started
|
||||
await tester.openPage(gettingStarted);
|
||||
|
||||
// Navigate back to the document
|
||||
await tester.openPage('insert_a_reference_${ViewLayoutPB.Grid.name}');
|
||||
|
||||
// We see only 25 Grid Rows
|
||||
expect(find.byType(GridRow), findsNWidgets(25));
|
||||
|
||||
// We see Add row and load more button
|
||||
expect(find.byType(GridAddRowButton), findsOneWidget);
|
||||
expect(find.byType(GridRowLoadMoreButton), findsOneWidget);
|
||||
|
||||
// Load more rows, expect 50 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 50);
|
||||
|
||||
// Load more rows, expect 75 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 75);
|
||||
|
||||
// Load more rows, expect 100 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 100);
|
||||
|
||||
// Load more rows, expect 103 visible
|
||||
await _loadMoreRows(tester, editorScrollable, 103);
|
||||
|
||||
// We no longer see load more option
|
||||
expect(find.byType(GridRowLoadMoreButton), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadMoreRows(
|
||||
WidgetTester tester,
|
||||
Finder scrollable, [
|
||||
int? expectedRows,
|
||||
]) async {
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(GridRowLoadMoreButton),
|
||||
100,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(GridRowLoadMoreButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
if (expectedRows != null) {
|
||||
expect(find.byType(GridRow), findsNWidgets(expectedRows));
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a referenced database of [layout] into the document
|
||||
Future<void> insertLinkedDatabase(
|
||||
WidgetTester tester,
|
||||
|
||||
+342
-1
@@ -1,14 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
@@ -18,7 +25,7 @@ void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('date or reminder block in document', () {
|
||||
group('date or reminder block in document:', () {
|
||||
testWidgets("insert date with time block", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -121,5 +128,339 @@ void main() {
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("copy, cut and paste a date mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'copy, cut and paste a date mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final DateTime currentDateTime = DateTime.now();
|
||||
final String formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
|
||||
|
||||
// get current date in editor
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// update selection and copy
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
|
||||
// update selection and cut
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: 1),
|
||||
end: Position(path: [0], offset: 2),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets("copy, cut and paste a reminder mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'copy, cut and paste a reminder mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
// trigger popup
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(TableCalendar),
|
||||
matching: find.text(15.toString()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a reminder
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.textContaining(
|
||||
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// verify
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and copy
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
|
||||
expect(
|
||||
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
|
||||
2,
|
||||
);
|
||||
|
||||
// update selection and cut
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: 1),
|
||||
end: Position(path: [0], offset: 2),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
|
||||
expect(
|
||||
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("delete, undo and redo a reminder mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'delete, undo and redo a reminder mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
// trigger popup
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(TableCalendar),
|
||||
matching: find.text(15.toString()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a reminder
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.textContaining(
|
||||
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// verify
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and backspace to delete the mention
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNothing);
|
||||
expect(find.text('@$formattedDate'), findsNothing);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
|
||||
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
|
||||
|
||||
// undo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNothing);
|
||||
expect(find.text('@$formattedDate'), findsNothing);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
|
||||
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
-99
@@ -7,88 +7,25 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
hide UploadImageMenu, ResizableImage;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:run_with_network_images/run_with_network_images.dart';
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
const _testImageUrls = [
|
||||
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640',
|
||||
'https://mathiasbynens.be/demo/animated-webp-supported.webp',
|
||||
'https://www.easygifanimator.net/images/samples/eglite.gif',
|
||||
'https://people.math.sc.edu/Burkardt/data/bmp/snail.bmp',
|
||||
'https://file-examples.com/storage/fe9566cb7d67345489a5a97/2017/10/file_example_JPG_100kB.jpg',
|
||||
];
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('image block in document', () {
|
||||
Future<void> testEmbedImage(WidgetTester tester, String url) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
);
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
url,
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.text(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], url);
|
||||
}
|
||||
|
||||
testWidgets('insert an image from local file', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -139,42 +76,6 @@ void main() {
|
||||
file.deleteSync();
|
||||
});
|
||||
|
||||
for (final url in _testImageUrls) {
|
||||
testWidgets('insert an image from network: $url', (tester) async {
|
||||
await testEmbedImage(tester, url);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('insert an image from unsplash', (tester) async {
|
||||
await runWithNetworkImages(() async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
expect(find.text('Unsplash'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('insert two images from local file at once', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
+50
@@ -113,5 +113,55 @@ void main() {
|
||||
|
||||
tester.expectToSeeText(formula);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline math equation and type something after it',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'math equation',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a inline page
|
||||
const formula = 'E = MC ^ 2';
|
||||
await tester.ime.insertText(formula);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
final inlineMathEquation = find.byType(InlineMathEquation);
|
||||
expect(inlineMathEquation, findsOneWidget);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
const text = 'Hello World';
|
||||
await tester.ime.insertText(text);
|
||||
|
||||
final inlineText = find.textContaining(text, findRichText: true);
|
||||
expect(inlineText, findsOneWidget);
|
||||
|
||||
// the text should be in the same line with the math equation
|
||||
final inlineMathEquationPosition = tester.getRect(inlineMathEquation);
|
||||
final textPosition = tester.getRect(inlineText);
|
||||
// allow 5px difference
|
||||
expect(
|
||||
(textPosition.top - inlineMathEquationPosition.top).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
expect(
|
||||
(textPosition.bottom - inlineMathEquationPosition.bottom).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+14
@@ -94,6 +94,20 @@ void main() {
|
||||
await tester.tapButton(finder);
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('insert a inline page and type something after the page',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await insertInlinePage(tester, ViewLayoutPB.Grid);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
const text = 'Hello World';
|
||||
await tester.ime.insertText(text);
|
||||
|
||||
expect(find.textContaining(text, findRichText: true), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ void main() {
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
|
||||
offset: 100,
|
||||
offset: 80,
|
||||
);
|
||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||
@@ -146,7 +146,7 @@ void main() {
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
|
||||
offset: 100,
|
||||
offset: 80,
|
||||
);
|
||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||
|
||||
+783
@@ -0,0 +1,783 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
const String heading1 = "Heading 1";
|
||||
const String heading2 = "Heading 2";
|
||||
const String heading3 = "Heading 3";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('simple table block test:', () {
|
||||
testWidgets('insert a simple table block', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
// validate the table is inserted
|
||||
expect(find.byType(SimpleTableBlockWidget), findsOneWidget);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
expect(
|
||||
editorState.selection,
|
||||
// table -> row -> cell -> paragraph
|
||||
Selection.collapsed(Position(path: [0, 0, 0, 0])),
|
||||
);
|
||||
|
||||
final firstCell = find.byType(SimpleTableCellBlockWidget).first;
|
||||
expect(
|
||||
tester
|
||||
.state<SimpleTableCellBlockWidgetState>(firstCell)
|
||||
.isEditingCellNotifier
|
||||
.value,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('select all in table cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
const cell1Content = 'Cell 1';
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText('New Table');
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapLineOfEditorAt(1);
|
||||
await tester.insertTableInDocument();
|
||||
await tester.ime.insertText(cell1Content);
|
||||
await tester.pumpAndSettle();
|
||||
// Select all in the cell
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed: !UniversalPlatform.isMacOS,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().selection,
|
||||
Selection(
|
||||
start: Position(path: [1, 0, 0, 0]),
|
||||
end: Position(path: [1, 0, 0, 0], offset: cell1Content.length),
|
||||
),
|
||||
);
|
||||
|
||||
// Press select all again, the selection should be the entire document
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed: !UniversalPlatform.isMacOS,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
expect(
|
||||
tester.editor.getCurrentEditorState().selection,
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [1, 1, 1, 0]),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. hover on the table
|
||||
1.1 click the add row button
|
||||
1.2 click the add column button
|
||||
1.3 click the add row and column button
|
||||
2. validate the table is updated
|
||||
3. delete the last column
|
||||
4. delete the last row
|
||||
5. validate the table is updated
|
||||
''', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
// add a new row
|
||||
final row = find.byWidgetPredicate((w) {
|
||||
return w is SimpleTableRowBlockWidget && w.node.rowIndex == 1;
|
||||
});
|
||||
await tester.hoverOnWidget(
|
||||
row,
|
||||
onHover: () async {
|
||||
final addRowButton = find.byType(SimpleTableAddRowButton).first;
|
||||
await tester.tap(addRowButton);
|
||||
},
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a new column
|
||||
final column = find.byWidgetPredicate((w) {
|
||||
return w is SimpleTableCellBlockWidget && w.node.columnIndex == 1;
|
||||
}).first;
|
||||
await tester.hoverOnWidget(
|
||||
column,
|
||||
onHover: () async {
|
||||
final addColumnButton = find.byType(SimpleTableAddColumnButton).first;
|
||||
await tester.tap(addColumnButton);
|
||||
},
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a new row and a new column
|
||||
final row2 = find.byWidgetPredicate((w) {
|
||||
return w is SimpleTableCellBlockWidget &&
|
||||
w.node.rowIndex == 2 &&
|
||||
w.node.columnIndex == 2;
|
||||
}).first;
|
||||
await tester.hoverOnWidget(
|
||||
row2,
|
||||
onHover: () async {
|
||||
// click the add row and column button
|
||||
final addRowAndColumnButton =
|
||||
find.byType(SimpleTableAddColumnAndRowButton).first;
|
||||
await tester.tap(addRowAndColumnButton);
|
||||
},
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tableNode =
|
||||
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
|
||||
expect(tableNode.columnLength, 4);
|
||||
expect(tableNode.rowLength, 4);
|
||||
|
||||
// delete the last row
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: tableNode.rowLength - 1,
|
||||
action: SimpleTableMoreAction.delete,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(tableNode.columnLength, 4);
|
||||
|
||||
// delete the last column
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: tableNode.columnLength - 1,
|
||||
action: SimpleTableMoreAction.delete,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tableNode.columnLength, 3);
|
||||
expect(tableNode.rowLength, 3);
|
||||
});
|
||||
|
||||
testWidgets('enable header column and header row', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
// enable the header row
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.enableHeaderRow,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
// enable the header column
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.enableHeaderColumn,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tableNode =
|
||||
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
|
||||
|
||||
expect(tableNode.isHeaderColumnEnabled, isTrue);
|
||||
expect(tableNode.isHeaderRowEnabled, isTrue);
|
||||
|
||||
// disable the header row
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.enableHeaderRow,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tableNode.isHeaderColumnEnabled, isTrue);
|
||||
expect(tableNode.isHeaderRowEnabled, isFalse);
|
||||
|
||||
// disable the header column
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.enableHeaderColumn,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tableNode.isHeaderColumnEnabled, isFalse);
|
||||
expect(tableNode.isHeaderRowEnabled, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('duplicate a column / row', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
// duplicate the row
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.duplicate,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// duplicate the column
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.duplicate,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tableNode =
|
||||
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
|
||||
expect(tableNode.columnLength, 3);
|
||||
expect(tableNode.rowLength, 3);
|
||||
});
|
||||
|
||||
testWidgets('insert left / insert right', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
// insert left
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.insertLeft,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insert right
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.insertRight,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tableNode =
|
||||
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
|
||||
expect(tableNode.columnLength, 4);
|
||||
expect(tableNode.rowLength, 2);
|
||||
});
|
||||
|
||||
testWidgets('insert above / insert below', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
// insert above
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.insertAbove,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// insert below
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.insertBelow,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final tableNode =
|
||||
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
|
||||
expect(tableNode.rowLength, 4);
|
||||
expect(tableNode.columnLength, 2);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('set column width to page width (1)', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
final tableNode = tester.editor.getNodeAtPath([0]);
|
||||
final beforeWidth = tableNode.width;
|
||||
|
||||
// set the column width to page width
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.setToPageWidth,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterWidth = tableNode.width;
|
||||
expect(afterWidth, greaterThan(beforeWidth));
|
||||
});
|
||||
|
||||
testWidgets('set column width to page width (2)', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
final tableNode = tester.editor.getNodeAtPath([0]);
|
||||
final beforeWidth = tableNode.width;
|
||||
|
||||
// set the column width to page width
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.setToPageWidth,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterWidth = tableNode.width;
|
||||
expect(afterWidth, greaterThan(beforeWidth));
|
||||
});
|
||||
|
||||
testWidgets('distribute columns evenly (1)', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
final tableNode = tester.editor.getNodeAtPath([0]);
|
||||
final beforeWidth = tableNode.width;
|
||||
|
||||
// set the column width to page width
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.row,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.distributeColumnsEvenly,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterWidth = tableNode.width;
|
||||
expect(afterWidth, equals(beforeWidth));
|
||||
|
||||
final distributeColumnWidthsEvenly =
|
||||
tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly];
|
||||
expect(distributeColumnWidthsEvenly, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('distribute columns evenly (2)', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
final tableNode = tester.editor.getNodeAtPath([0]);
|
||||
final beforeWidth = tableNode.width;
|
||||
|
||||
// set the column width to page width
|
||||
await tester.clickMoreActionItemInTableMenu(
|
||||
type: SimpleTableMoreActionType.column,
|
||||
index: 0,
|
||||
action: SimpleTableMoreAction.distributeColumnsEvenly,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterWidth = tableNode.width;
|
||||
expect(afterWidth, equals(beforeWidth));
|
||||
|
||||
final distributeColumnWidthsEvenly =
|
||||
tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly];
|
||||
expect(distributeColumnWidthsEvenly, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('using option menu to set column width', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final beforeWidth = editorState.document.nodeAtPath([0])!.width;
|
||||
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterWidth = editorState.document.nodeAtPath([0])!.width;
|
||||
expect(afterWidth, greaterThan(beforeWidth));
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys
|
||||
.document_plugins_simpleTable_moreActions_distributeColumnsWidth
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterWidth2 = editorState.document.nodeAtPath([0])!.width;
|
||||
expect(afterWidth2, equals(afterWidth));
|
||||
});
|
||||
|
||||
testWidgets('insert a table and use select all the delete it',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(1);
|
||||
await tester.ime.insertText('Hello World');
|
||||
|
||||
// select all
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
isControlPressed: !UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// only one paragraph left
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final paragraphNode = editorState.document.nodeAtPath([0])!;
|
||||
expect(paragraphNode.delta, isNull);
|
||||
});
|
||||
|
||||
testWidgets('use tab or shift+tab to navigate in table', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final selection = editorState.selection;
|
||||
expect(selection, isNotNull);
|
||||
expect(selection!.start.path, [0, 0, 1, 0]);
|
||||
expect(selection.end.path, [0, 0, 1, 0]);
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.tab,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final selection2 = editorState.selection;
|
||||
expect(selection2, isNotNull);
|
||||
expect(selection2!.start.path, [0, 0, 0, 0]);
|
||||
expect(selection2.end.path, [0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
testWidgets('shift+enter to insert a new line in table', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.enter,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.document.nodeAtPath([0, 0, 0])!;
|
||||
expect(node.children.length, 1);
|
||||
});
|
||||
|
||||
testWidgets('using option menu to set table align', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(beforeAlign, TableAlign.left);
|
||||
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_center.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign, TableAlign.center);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_right.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign2, TableAlign.right);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_left.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign3, TableAlign.left);
|
||||
});
|
||||
|
||||
testWidgets('using option menu to set table align', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(beforeAlign, TableAlign.left);
|
||||
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_center.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign, TableAlign.center);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_right.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign2, TableAlign.right);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_left.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign3, TableAlign.left);
|
||||
});
|
||||
|
||||
testWidgets('support slash menu in table', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
final path = [0, 0, 0, 0];
|
||||
final selection = Selection.collapsed(Position(path: path));
|
||||
editorState.selection = selection;
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final paragraphItem = find.byWidgetPredicate((w) {
|
||||
return w is SelectionMenuItemWidget &&
|
||||
w.item.name == LocaleKeys.document_slashMenu_name_text.tr();
|
||||
});
|
||||
expect(paragraphItem, findsOneWidget);
|
||||
|
||||
await tester.tap(paragraphItem);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final paragraphNode = editorState.document.nodeAtPath(path)!;
|
||||
expect(paragraphNode.type, equals(ParagraphBlockKeys.type));
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
/// Insert a table in the document
|
||||
Future<void> insertTableInDocument() async {
|
||||
// open the actions menu and insert the outline block
|
||||
await editor.showSlashMenu();
|
||||
await editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> clickMoreActionItemInTableMenu({
|
||||
required SimpleTableMoreActionType type,
|
||||
required int index,
|
||||
required SimpleTableMoreAction action,
|
||||
}) async {
|
||||
if (type == SimpleTableMoreActionType.row) {
|
||||
final row = find.byWidgetPredicate((w) {
|
||||
return w is SimpleTableRowBlockWidget && w.node.rowIndex == index;
|
||||
});
|
||||
await hoverOnWidget(
|
||||
row,
|
||||
onHover: () async {
|
||||
final moreActionButton = find.byWidgetPredicate((w) {
|
||||
return w is SimpleTableMoreActionMenu &&
|
||||
w.type == SimpleTableMoreActionType.row &&
|
||||
w.index == index;
|
||||
});
|
||||
await tapButton(moreActionButton);
|
||||
await tapButton(find.text(action.name));
|
||||
},
|
||||
);
|
||||
await pumpAndSettle();
|
||||
} else if (type == SimpleTableMoreActionType.column) {
|
||||
final column = find.byWidgetPredicate((w) {
|
||||
return w is SimpleTableCellBlockWidget && w.node.columnIndex == index;
|
||||
}).first;
|
||||
await hoverOnWidget(
|
||||
column,
|
||||
onHover: () async {
|
||||
final moreActionButton = find.byWidgetPredicate((w) {
|
||||
return w is SimpleTableMoreActionMenu &&
|
||||
w.type == SimpleTableMoreActionType.column &&
|
||||
w.index == index;
|
||||
});
|
||||
await tapButton(moreActionButton);
|
||||
await tapButton(find.text(action.name));
|
||||
},
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
await tapAt(Offset.zero);
|
||||
}
|
||||
}
|
||||
+21
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -263,5 +265,24 @@ void main() {
|
||||
expect(node.attributes[ToggleListBlockKeys.level], 3);
|
||||
expect(node.delta!.toPlainText(), 'Hello');
|
||||
});
|
||||
|
||||
testWidgets('click the toggle list to create a new paragraph',
|
||||
(tester) async {
|
||||
await prepareToggleHeadingBlock(tester, '> # Hello');
|
||||
final emptyHintText = find.text(
|
||||
LocaleKeys.document_plugins_emptyToggleHeading.tr(
|
||||
args: ['1'],
|
||||
),
|
||||
);
|
||||
expect(emptyHintText, findsOneWidget);
|
||||
|
||||
await tester.tapButton(emptyHintText);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// check the new paragraph is created
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0, 0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
}
|
||||
+6
-4
@@ -35,10 +35,12 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
),
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
)
|
||||
.first,
|
||||
onHover: () async {
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
+21
-8
@@ -2,10 +2,10 @@ import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
@@ -23,7 +23,7 @@ void main() {
|
||||
.last;
|
||||
}
|
||||
|
||||
group('sign-in page settings: ', () {
|
||||
group('sign-in page settings:', () {
|
||||
testWidgets('change server type', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
@@ -45,28 +45,36 @@ void main() {
|
||||
|
||||
// change the server type to self-host
|
||||
await tester.tapButton(appflowyCloudType);
|
||||
final selfhostedButton = findServerType(
|
||||
final selfHostedButton = findServerType(
|
||||
AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapButton(selfhostedButton);
|
||||
await tester.tapButton(selfHostedButton);
|
||||
|
||||
// update server url
|
||||
const serverUrl = 'https://test.appflowy.cloud';
|
||||
const serverUrl = 'https://self-hosted.appflowy.cloud';
|
||||
await tester.enterText(
|
||||
find.byKey(kSelfHostedTextInputFieldKey),
|
||||
serverUrl,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
// update the web url
|
||||
const webUrl = 'https://self-hosted.appflowy.com';
|
||||
await tester.enterText(
|
||||
find.byKey(kSelfHostedWebTextInputFieldKey),
|
||||
webUrl,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(LocaleKeys.button_save.tr()),
|
||||
);
|
||||
|
||||
// wait the app to restart, and the tooltip to disappear
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpUntilNotFound(find.byType(DesktopToast));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// open settings page to check the result
|
||||
await tester.tapButton(settingsButton);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
expect(
|
||||
@@ -78,18 +86,23 @@ void main() {
|
||||
find.text(serverUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
// check the web url
|
||||
expect(
|
||||
find.text(webUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// reset to appflowy cloud
|
||||
await tester.tapButton(
|
||||
findServerType(AuthenticatorType.appflowyCloudSelfHost),
|
||||
);
|
||||
);
|
||||
// change the server type to appflowy cloud
|
||||
await tester.tapButton(
|
||||
findServerType(AuthenticatorType.appflowyCloud),
|
||||
);
|
||||
|
||||
// wait the app to restart, and the tooltip to disappear
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpUntilNotFound(find.byType(DesktopToast));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -44,5 +48,82 @@ void main() {
|
||||
);
|
||||
expect(isExpanded(type: FolderSpaceType.private), true);
|
||||
});
|
||||
|
||||
testWidgets('Expanding with subpage', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
const page1 = 'SubPageBloc', page2 = '$page1 2';
|
||||
await tester.createNewPageWithNameUnderParent(name: page1);
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: page2,
|
||||
parentName: page1,
|
||||
);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: gettingStarted,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.tapNewPageButton();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final slashMenu = find
|
||||
.ancestor(
|
||||
of: find.byType(SelectionMenuItemWidget),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is Scrollable,
|
||||
),
|
||||
)
|
||||
.first;
|
||||
final slashMenuItem = find.text(
|
||||
LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
slashMenuItem,
|
||||
100,
|
||||
scrollable: slashMenu,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
);
|
||||
|
||||
final menuItemFinder = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is SelectionMenuItemWidget &&
|
||||
w.item.name == LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
|
||||
);
|
||||
|
||||
final menuItem =
|
||||
menuItemFinder.evaluate().first.widget as SelectionMenuItemWidget;
|
||||
|
||||
/// tapSlashMenuItemWithName is not working, so invoke this function directly
|
||||
menuItem.item.handler(
|
||||
menuItem.editorState,
|
||||
menuItem.menuService,
|
||||
menuItemFinder.evaluate().first,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
final actionHandler = find.byType(InlineActionsHandler);
|
||||
final subPage = find.descendant(
|
||||
of: actionHandler,
|
||||
matching: find.text(page2, findRichText: true),
|
||||
);
|
||||
await tester.tapButton(subPage);
|
||||
|
||||
final subpageBlock = find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text(page2, findRichText: true),
|
||||
);
|
||||
|
||||
expect(find.text(page2, findRichText: true), findsOneWidget);
|
||||
await tester.tapButton(subpageBlock);
|
||||
|
||||
/// one is in SectionFolder, another one is in CoverTitle
|
||||
/// the last one is in FlowyNavigation
|
||||
expect(find.text(page2, findRichText: true), findsNWidgets(3));
|
||||
});
|
||||
});
|
||||
}
|
||||
+53
@@ -196,5 +196,58 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'reorder favorites',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// there are no favorite views
|
||||
final favorites = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.byType(ViewItem),
|
||||
);
|
||||
expect(favorites, findsNothing);
|
||||
|
||||
/// create views and then favorite them
|
||||
const pageNames = ['001', '002', '003'];
|
||||
for (final name in pageNames) {
|
||||
await tester.createNewPageWithNameUnderParent(name: name);
|
||||
}
|
||||
for (final name in pageNames) {
|
||||
await tester.favoriteViewByName(name);
|
||||
}
|
||||
expect(favorites, findsNWidgets(pageNames.length));
|
||||
|
||||
final oldNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(oldNames, pageNames);
|
||||
|
||||
/// drag first to last
|
||||
await tester.reorderFavorite(
|
||||
fromName: '001',
|
||||
toName: '003',
|
||||
);
|
||||
List<String> newNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(newNames, ['002', '003', '001']);
|
||||
|
||||
/// drag first to second
|
||||
await tester.reorderFavorite(
|
||||
fromName: '002',
|
||||
toName: '003',
|
||||
);
|
||||
newNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(newNames, ['003', '002', '001']);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
+191
-62
@@ -1,83 +1,212 @@
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/common_operations.dart';
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/expectation.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final emoji = EmojiIconData.emoji('😁');
|
||||
|
||||
const emoji = '😁';
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
group('Icon:', () {
|
||||
testWidgets('Update page icon in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
testWidgets('Update page emoji in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
testWidgets('Update page icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
// update its emoji
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
testWidgets('Update page emoji in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: value.name,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
|
||||
tester.expectViewTitleHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
// update its emoji
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: value.name,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
|
||||
tester.expectViewTitleHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Emoji Search Bar Get Focus', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
await tester.openPage(
|
||||
value.name,
|
||||
layout: value,
|
||||
);
|
||||
final title = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(value.name),
|
||||
);
|
||||
await tester.tapButton(title);
|
||||
await tester.tapButton(find.byType(EmojiPickerButton));
|
||||
|
||||
final emojiPicker = find.byType(FlowyEmojiPicker);
|
||||
expect(emojiPicker, findsOneWidget);
|
||||
final textField = find.descendant(
|
||||
of: emojiPicker,
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
expect(textField, findsOneWidget);
|
||||
final textFieldWidget =
|
||||
textField.evaluate().first.widget as FlowyTextField;
|
||||
assert(textFieldWidget.focusNode!.hasFocus);
|
||||
await tester.tapEmoji(emoji.emoji);
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page icon in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
icon: iconData,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
iconData,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: value.name,
|
||||
layout: value,
|
||||
icon: iconData,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
iconData,
|
||||
);
|
||||
|
||||
tester.expectViewTitleHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
iconData,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/common_operations.dart';
|
||||
import '../../shared/expectation.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Skip the empty group name icon in recent icons', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// clear local data
|
||||
RecentIcons.clear();
|
||||
await loadIconGroups();
|
||||
final groups = kIconGroups!;
|
||||
final List<RecentIcon> localIcons = [];
|
||||
for (final e in groups) {
|
||||
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
|
||||
}
|
||||
await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, ''));
|
||||
await tester.openPage(gettingStarted);
|
||||
final title = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(gettingStarted),
|
||||
);
|
||||
await tester.tapButton(title);
|
||||
|
||||
/// tap emoji picker button
|
||||
await tester.tapButton(find.byType(EmojiPickerButton));
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
|
||||
|
||||
/// tap icon tab
|
||||
final pickTab = find.byType(PickerTab);
|
||||
final iconTab = find.descendant(
|
||||
of: pickTab,
|
||||
matching: find.text(PickerTabType.icon.tr),
|
||||
);
|
||||
await tester.tapButton(iconTab);
|
||||
|
||||
expect(find.byType(FlowyIconPicker), findsOneWidget);
|
||||
|
||||
/// no recent icons
|
||||
final recentText = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: find.text('Recent'),
|
||||
);
|
||||
expect(recentText, findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
||||
import 'sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar_recent_icon_test.dart' as sidebar_recent_icon_test;
|
||||
import 'sidebar_test.dart' as sidebar_test;
|
||||
import 'sidebar_view_item_test.dart' as sidebar_view_item_test;
|
||||
|
||||
@@ -14,4 +15,5 @@ void main() {
|
||||
sidebar_favorite_test.main();
|
||||
sidebar_icon_test.main();
|
||||
sidebar_view_item_test.main();
|
||||
sidebar_recent_icon_test.main();
|
||||
}
|
||||
+15
-2
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -11,7 +13,14 @@ import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Sidebar view item tests', () {
|
||||
testWidgets('Access view item context menu by right click', (tester) async {
|
||||
@@ -38,7 +47,11 @@ void main() {
|
||||
await tester.tapEmoji(emoji);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectViewHasIcon(gettingStarted, ViewLayoutPB.Document, emoji);
|
||||
tester.expectViewHasIcon(
|
||||
gettingStarted,
|
||||
ViewLayoutPB.Document,
|
||||
EmojiIconData.emoji(emoji),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/common_operations.dart';
|
||||
import '../../shared/document_test_operations.dart';
|
||||
import '../document/document_codeblock_paste_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('Code Block Language Selector Test', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
/// tap editor to get focus
|
||||
await tester.tapButton(find.byType(AppFlowyEditor));
|
||||
|
||||
expect(find.byType(CodeBlockLanguageSelector), findsNothing);
|
||||
await insertCodeBlockInDocument(tester);
|
||||
|
||||
///tap button
|
||||
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
|
||||
await tester
|
||||
.tapButtonWithName(LocaleKeys.document_codeBlock_language_auto.tr());
|
||||
expect(find.byType(CodeBlockLanguageSelector), findsOneWidget);
|
||||
|
||||
for (var i = 0; i < 3; ++i) {
|
||||
await onKey(tester, LogicalKeyboardKey.arrowDown);
|
||||
}
|
||||
for (var i = 0; i < 2; ++i) {
|
||||
await onKey(tester, LogicalKeyboardKey.arrowUp);
|
||||
}
|
||||
|
||||
await onKey(tester, LogicalKeyboardKey.enter);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
String language = editorState
|
||||
.getNodeAtPath([0])!
|
||||
.attributes[CodeBlockKeys.language]
|
||||
.toString();
|
||||
expect(
|
||||
language.toLowerCase(),
|
||||
defaultCodeBlockSupportedLanguages.first.toLowerCase(),
|
||||
);
|
||||
|
||||
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
|
||||
await tester.tapButtonWithName(language);
|
||||
|
||||
await onKey(tester, LogicalKeyboardKey.arrowUp);
|
||||
await onKey(tester, LogicalKeyboardKey.enter);
|
||||
|
||||
language = editorState
|
||||
.getNodeAtPath([0])!
|
||||
.attributes[CodeBlockKeys.language]
|
||||
.toString();
|
||||
expect(
|
||||
language.toLowerCase(),
|
||||
defaultCodeBlockSupportedLanguages.last.toLowerCase(),
|
||||
);
|
||||
|
||||
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
|
||||
await tester.tapButtonWithName(language);
|
||||
tester.testTextInput.enterText("rust");
|
||||
await onKey(tester, LogicalKeyboardKey.delete);
|
||||
await onKey(tester, LogicalKeyboardKey.delete);
|
||||
await onKey(tester, LogicalKeyboardKey.arrowDown);
|
||||
tester.testTextInput.enterText("st");
|
||||
await onKey(tester, LogicalKeyboardKey.arrowDown);
|
||||
await onKey(tester, LogicalKeyboardKey.enter);
|
||||
language = editorState
|
||||
.getNodeAtPath([0])!
|
||||
.attributes[CodeBlockKeys.language]
|
||||
.toString();
|
||||
expect(language.toLowerCase(), 'rust');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onKey(WidgetTester tester, LogicalKeyboardKey key) async {
|
||||
await tester.simulateKeyEvent(key);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -87,7 +88,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
importedPageEditorState.getNodeAtPath([2])!.type,
|
||||
TableBlockKeys.type,
|
||||
SimpleTableBlockKeys.type,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -145,5 +150,222 @@ void main() {
|
||||
// and in this case view name in sidebar)
|
||||
expect(find.text(gettingStarted), findsNWidgets(3));
|
||||
});
|
||||
|
||||
testWidgets('cannot close pinned tabs', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(TabsManager),
|
||||
matching: find.byType(TabBar),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(name: _documentName);
|
||||
await tester.createNewPageWithNameUnderParent(name: _documentTwoName);
|
||||
|
||||
// Open second menu item in a new tab
|
||||
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
|
||||
|
||||
// Open third menu item in a new tab
|
||||
await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(TabsManager),
|
||||
matching: find.byType(FlowyTab),
|
||||
),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
|
||||
const firstTab = _documentTwoName;
|
||||
const secondTab = gettingStarted;
|
||||
const thirdTab = _documentName;
|
||||
|
||||
expect(tester.isTabAtIndex(firstTab, 0), isTrue);
|
||||
expect(tester.isTabAtIndex(secondTab, 1), isTrue);
|
||||
expect(tester.isTabAtIndex(thirdTab, 2), isTrue);
|
||||
|
||||
expect(tester.isTabPinned(gettingStarted), isFalse);
|
||||
|
||||
// Right click on second tab
|
||||
await tester.openTabMenu(gettingStarted);
|
||||
expect(find.byType(TabMenu), findsOneWidget);
|
||||
|
||||
// Pin second tab
|
||||
await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.isTabPinned(gettingStarted), isTrue);
|
||||
|
||||
/// Right click on first unpinned tab (second tab)
|
||||
await tester.openTabMenu(_documentTwoName);
|
||||
|
||||
// Close others
|
||||
await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// We expect to find 2 tabs, the first pinned tab and the second tab
|
||||
expect(find.byType(FlowyTab), findsNWidgets(2));
|
||||
expect(tester.isTabAtIndex(gettingStarted, 0), isTrue);
|
||||
expect(tester.isTabAtIndex(_documentTwoName, 1), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('pin/unpin tabs proper order', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(TabsManager),
|
||||
matching: find.byType(TabBar),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(name: _documentName);
|
||||
await tester.createNewPageWithNameUnderParent(name: _documentTwoName);
|
||||
|
||||
// Open second menu item in a new tab
|
||||
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
|
||||
|
||||
// Open third menu item in a new tab
|
||||
await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(TabsManager),
|
||||
matching: find.byType(FlowyTab),
|
||||
),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
|
||||
const firstTabName = _documentTwoName;
|
||||
const secondTabName = gettingStarted;
|
||||
const thirdTabName = _documentName;
|
||||
|
||||
// Expect correct order
|
||||
expect(tester.isTabAtIndex(firstTabName, 0), isTrue);
|
||||
expect(tester.isTabAtIndex(secondTabName, 1), isTrue);
|
||||
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
|
||||
|
||||
// Pin second tab
|
||||
await tester.openTabMenu(secondTabName);
|
||||
await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.isTabPinned(secondTabName), isTrue);
|
||||
|
||||
// Expect correct order
|
||||
expect(tester.isTabAtIndex(secondTabName, 0), isTrue);
|
||||
expect(tester.isTabAtIndex(firstTabName, 1), isTrue);
|
||||
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
|
||||
|
||||
// Pin new second tab (first tab)
|
||||
await tester.openTabMenu(firstTabName);
|
||||
await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.isTabPinned(firstTabName), isTrue);
|
||||
expect(tester.isTabPinned(secondTabName), isTrue);
|
||||
expect(tester.isTabPinned(thirdTabName), isFalse);
|
||||
|
||||
expect(tester.isTabAtIndex(secondTabName, 0), isTrue);
|
||||
expect(tester.isTabAtIndex(firstTabName, 1), isTrue);
|
||||
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
|
||||
|
||||
// Unpin second tab
|
||||
await tester.openTabMenu(secondTabName);
|
||||
await tester.tap(find.text(LocaleKeys.tabMenu_unpinTab.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.isTabPinned(firstTabName), isTrue);
|
||||
expect(tester.isTabPinned(secondTabName), isFalse);
|
||||
expect(tester.isTabPinned(thirdTabName), isFalse);
|
||||
|
||||
expect(tester.isTabAtIndex(firstTabName, 0), isTrue);
|
||||
expect(tester.isTabAtIndex(secondTabName, 1), isTrue);
|
||||
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('displaying icons in tab', (tester) async {
|
||||
RecentIcons.enable = false;
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
final icon = await tester.loadIcon();
|
||||
// update emoji
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: gettingStarted,
|
||||
parentName: gettingStarted,
|
||||
layout: ViewLayoutPB.Document,
|
||||
icon: icon,
|
||||
);
|
||||
|
||||
/// create new page
|
||||
await tester.createNewPageWithNameUnderParent(name: _documentName);
|
||||
|
||||
/// open new tab for [gettingStarted]
|
||||
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
|
||||
|
||||
final tabs = find.descendant(
|
||||
of: find.byType(TabsManager),
|
||||
matching: find.byType(FlowyTab),
|
||||
);
|
||||
expect(tabs, findsNWidgets(2));
|
||||
|
||||
final svgInTab =
|
||||
find.descendant(of: tabs.last, matching: find.byType(FlowySvg));
|
||||
final svgWidget = svgInTab.evaluate().first.widget as FlowySvg;
|
||||
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
|
||||
expect(svgWidget.svgString, iconsData.svgString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension _TabsTester on WidgetTester {
|
||||
bool isTabPinned(String tabName) {
|
||||
final tabFinder = find.ancestor(
|
||||
of: find.byWidgetPredicate(
|
||||
(w) => w is ViewTabBarItem && w.view.name == tabName,
|
||||
),
|
||||
matching: find.byType(FlowyTab),
|
||||
);
|
||||
|
||||
final FlowyTab tabWidget = widget(tabFinder);
|
||||
return tabWidget.pageManager.isPinned;
|
||||
}
|
||||
|
||||
bool isTabAtIndex(String tabName, int index) {
|
||||
final tabFinder = find.ancestor(
|
||||
of: find.byWidgetPredicate(
|
||||
(w) => w is ViewTabBarItem && w.view.name == tabName,
|
||||
),
|
||||
matching: find.byType(FlowyTab),
|
||||
);
|
||||
|
||||
final pluginId = (widget(tabFinder) as FlowyTab).pageManager.plugin.id;
|
||||
|
||||
final pluginIds = find
|
||||
.byType(FlowyTab)
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as FlowyTab).pageManager.plugin.id);
|
||||
|
||||
return pluginIds.elementAt(index) == pluginId;
|
||||
}
|
||||
|
||||
Future<void> openTabMenu(String tabName) async {
|
||||
await tap(
|
||||
buttons: kSecondaryButton,
|
||||
find.ancestor(
|
||||
of: find.byWidgetPredicate(
|
||||
(w) => w is ViewTabBarItem && w.view.name == tabName,
|
||||
),
|
||||
matching: find.byType(FlowyTab),
|
||||
),
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
|
||||
import 'desktop/uncategorized/code_block_language_selector_test.dart'
|
||||
as code_language_selector;
|
||||
import 'desktop/first_test/first_test.dart' as first_test;
|
||||
|
||||
Future<void> main() async {
|
||||
@@ -11,6 +13,6 @@ Future<void> runIntegration9OnDesktop() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
first_test.main();
|
||||
|
||||
tabs_test.main();
|
||||
code_language_selector.main();
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'page_style_test.dart' as page_style_test;
|
||||
import 'plus_menu_test.dart' as plus_menu_test;
|
||||
import 'simple_table_test.dart' as simple_table_test;
|
||||
import 'title_test.dart' as title_test;
|
||||
import 'toolbar_test.dart' as toolbar_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -11,4 +13,6 @@ void main() {
|
||||
title_test.main();
|
||||
page_style_test.main();
|
||||
plus_menu_test.main();
|
||||
simple_table_test.main();
|
||||
toolbar_test.main();
|
||||
}
|
||||
@@ -1,17 +1,30 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('document page style:', () {
|
||||
double getCurrentEditorFontSize() {
|
||||
@@ -114,5 +127,37 @@ void main() {
|
||||
);
|
||||
expect(builtInCover, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('page style icon', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
final createPageButton =
|
||||
find.byKey(BottomNavigationBarItemType.add.valueKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
|
||||
/// toggle the preset button
|
||||
await tester.tapSvgButton(FlowySvgs.m_layout_s);
|
||||
|
||||
/// select document plugins emoji
|
||||
final pageStyleIcon = find.byType(PageStyleIcon);
|
||||
|
||||
/// there should be none of emoji
|
||||
final noneText = find.text(LocaleKeys.pageStyle_none.tr());
|
||||
expect(noneText, findsOneWidget);
|
||||
await tester.tapButton(pageStyleIcon);
|
||||
|
||||
/// select an emoji
|
||||
const emoji = '😄';
|
||||
await tester.tapEmoji(emoji);
|
||||
await tester.tapSvgButton(FlowySvgs.m_layout_s);
|
||||
expect(noneText, findsNothing);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: pageStyleIcon,
|
||||
matching: find.text(emoji),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('simple table:', () {
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. insert a row above the table
|
||||
3. insert a row below the table
|
||||
4. insert a column left to the table
|
||||
5. insert a column right to the table
|
||||
6. delete the first row
|
||||
7. delete the first column
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
// insert left and insert right
|
||||
{
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// insert left, insert right
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertLeft.tr(),
|
||||
),
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertRight
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(4));
|
||||
}
|
||||
|
||||
// insert above and insert below
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertAbove
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertBelow
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.rowLength, equals(4));
|
||||
expect(table.columnLength, equals(4));
|
||||
}
|
||||
|
||||
// delete the first row
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// delete the first row
|
||||
await tester.clickRowMenuButton(0);
|
||||
await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.rowLength, equals(3));
|
||||
expect(table.columnLength, equals(4));
|
||||
}
|
||||
|
||||
// delete the first column
|
||||
{
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.clickColumnMenuButton(0);
|
||||
await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.rowLength, equals(3));
|
||||
expect(table.columnLength, equals(3));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. enable header column
|
||||
3. enable header row
|
||||
4. set to page width
|
||||
5. distribute columns evenly
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
// enable header column
|
||||
{
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_headerColumn
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// enable header row
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_headerRow.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.isHeaderColumnEnabled, isTrue);
|
||||
expect(table.isHeaderRowEnabled, isTrue);
|
||||
|
||||
// disable header column
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
final toggleButton = find.descendant(
|
||||
of: find.byType(SimpleTableHeaderActionButton),
|
||||
matching: find.byType(CupertinoSwitch),
|
||||
);
|
||||
await tester.tapButton(toggleButton);
|
||||
}
|
||||
|
||||
// enable header row
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
final toggleButton = find.descendant(
|
||||
of: find.byType(SimpleTableHeaderActionButton),
|
||||
matching: find.byType(CupertinoSwitch),
|
||||
);
|
||||
await tester.tapButton(toggleButton);
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
expect(table.isHeaderColumnEnabled, isFalse);
|
||||
expect(table.isHeaderRowEnabled, isFalse);
|
||||
|
||||
// set to page width
|
||||
{
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
final beforeWidth = table.width;
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
// check the table is updated
|
||||
expect(table.width, greaterThan(beforeWidth));
|
||||
}
|
||||
|
||||
// distribute columns evenly
|
||||
{
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
final beforeWidth = table.width;
|
||||
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// distribute columns evenly
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys
|
||||
.document_plugins_simpleTable_moreActions_distributeColumnsWidth
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
// check the table is updated
|
||||
expect(table.width, equals(beforeWidth));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. bold
|
||||
3. clear content
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
await tester.ime.insertText('Hello');
|
||||
|
||||
// enable bold
|
||||
{
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// enable bold
|
||||
await tester.clickSimpleTableBoldContentAction();
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the first cell is bold
|
||||
final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;
|
||||
expect(paragraph.isInBoldColumn, isTrue);
|
||||
}
|
||||
|
||||
// clear content
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
final clearContents = find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
|
||||
.tr(),
|
||||
);
|
||||
|
||||
// clear content
|
||||
final scrollable = find.descendant(
|
||||
of: find.byType(SimpleTableBottomSheet),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
clearContents,
|
||||
100,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await tester.tapButton(clearContents);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the first cell is empty
|
||||
final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;
|
||||
expect(paragraph.delta!, isEmpty);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. insert a heading block in table cell
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
// open the plus menu and select the heading block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.editor_heading1.tr(),
|
||||
);
|
||||
|
||||
// check the heading block is inserted
|
||||
final heading = editorState.getNodeAtPath([0, 0, 0, 0])!;
|
||||
expect(heading.type, equals(HeadingBlockKeys.type));
|
||||
expect(heading.level, equals(1));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. resize column
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final beforeWidth = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
|
||||
|
||||
// find the first cell
|
||||
{
|
||||
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
|
||||
final offset = tester.getCenter(resizeHandle);
|
||||
final gesture = await tester.startGesture(offset, pointer: 7);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.moveBy(const Offset(100, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
final afterWidth1 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
|
||||
expect(afterWidth1, greaterThan(beforeWidth));
|
||||
|
||||
// resize back to the original width
|
||||
{
|
||||
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
|
||||
final offset = tester.getCenter(resizeHandle);
|
||||
final gesture = await tester.startGesture(offset, pointer: 7);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.moveBy(const Offset(-100, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
final afterWidth2 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
|
||||
expect(afterWidth2, equals(beforeWidth));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('toolbar menu:', () {
|
||||
testWidgets('insert links', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
final createPageButton = find.byKey(
|
||||
BottomNavigationBarItemType.add.valueKey,
|
||||
);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
|
||||
final editor = find.byType(AppFlowyEditor);
|
||||
expect(editor, findsOneWidget);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
/// move cursor to content
|
||||
final root = editorState.document.root;
|
||||
final lastNode = root.children.lastOrNull;
|
||||
await editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: lastNode!.path)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// insert two lines of text
|
||||
const strFirst = 'FirstLine',
|
||||
strSecond = 'SecondLine',
|
||||
link = 'google.com';
|
||||
await editorState.insertTextAtCurrentSelection(strFirst);
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertNewLine();
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertTextAtCurrentSelection(strSecond);
|
||||
await tester.pumpAndSettle();
|
||||
final firstLine = find.text(strFirst, findRichText: true),
|
||||
secondLine = find.text(strSecond, findRichText: true);
|
||||
expect(firstLine, findsOneWidget);
|
||||
expect(secondLine, findsOneWidget);
|
||||
|
||||
/// select the first line
|
||||
await tester.longPress(firstLine);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// find aa item and tap it
|
||||
final aaItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is AppFlowyMobileToolbarIconItem &&
|
||||
widget.icon == FlowySvgs.m_toolbar_aa_m,
|
||||
);
|
||||
expect(aaItem, findsOneWidget);
|
||||
await tester.tapButton(aaItem);
|
||||
|
||||
/// find link button and tap it
|
||||
final linkButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is MobileToolbarMenuItemWrapper &&
|
||||
widget.icon == FlowySvgs.m_toolbar_link_m,
|
||||
);
|
||||
expect(linkButton, findsOneWidget);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// input the link
|
||||
final linkField = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is FlowyTextField &&
|
||||
w.hintText == LocaleKeys.document_inlineLink_url_placeholder.tr(),
|
||||
);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// complete inputting
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
/// do it again
|
||||
/// select the second line
|
||||
await tester.longPress(secondLine);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(aaItem);
|
||||
await tester.tapButton(linkButton);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
final firstNode = editorState.getNodeAtPath([0]);
|
||||
final secondNode = editorState.getNodeAtPath([1]);
|
||||
|
||||
Map commonDeltaJson(String insert) => {
|
||||
"insert": insert,
|
||||
"attributes": {"href": link},
|
||||
};
|
||||
|
||||
expect(
|
||||
firstNode?.delta?.toJson(),
|
||||
commonDeltaJson(strFirst),
|
||||
);
|
||||
expect(
|
||||
secondNode?.delta?.toJson(),
|
||||
commonDeltaJson(strSecond),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('Change default text direction', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
/// tap [Setting] button
|
||||
await tester.tapButton(find.byType(HomePageSettingsPopupMenu));
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr()));
|
||||
|
||||
/// tap [Default Text Direction]
|
||||
await tester.tapButton(
|
||||
find.text(LocaleKeys.settings_appearance_textDirection_label.tr()),
|
||||
);
|
||||
|
||||
/// there are 3 items: LTR-RTL-AUTO
|
||||
final bottomSheet = find.ancestor(
|
||||
of: find.byType(FlowyOptionTile),
|
||||
matching: find.byType(SafeArea),
|
||||
);
|
||||
final items = find.descendant(
|
||||
of: bottomSheet,
|
||||
matching: find.byType(FlowyOptionTile),
|
||||
);
|
||||
expect(items, findsNWidgets(3));
|
||||
|
||||
/// select [Auto]
|
||||
await tester.tapButton(items.last);
|
||||
expect(
|
||||
find.text(LocaleKeys.settings_appearance_textDirection_auto.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
/// go back home
|
||||
await tester.tapButton(find.byType(AppBarImmersiveBackButton));
|
||||
|
||||
/// create new page
|
||||
final createPageButton =
|
||||
find.byKey(BottomNavigationBarItemType.add.valueKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
const testEnglish = 'English', testArabic = 'إنجليزي';
|
||||
|
||||
/// insert [testEnglish]
|
||||
await editorState.insertTextAtCurrentSelection(testEnglish);
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertNewLine(position: editorState.selection!.end);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// insert [testArabic]
|
||||
await editorState.insertTextAtCurrentSelection(testArabic);
|
||||
await tester.pumpAndSettle();
|
||||
final testEnglishFinder = find.text(testEnglish, findRichText: true),
|
||||
testArabicFinder = find.text(testArabic, findRichText: true);
|
||||
final testEnglishRenderBox =
|
||||
testEnglishFinder.evaluate().first.renderObject as RenderBox,
|
||||
testArabicRenderBox =
|
||||
testArabicFinder.evaluate().first.renderObject as RenderBox;
|
||||
final englishPosition = testEnglishRenderBox.localToGlobal(Offset.zero),
|
||||
arabicPosition = testArabicRenderBox.localToGlobal(Offset.zero);
|
||||
expect(englishPosition.dx > arabicPosition.dx, true);
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'mobile/document/document_test_runner.dart' as document_test_runner;
|
||||
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
|
||||
import 'mobile/settings/default_text_direction_test.dart'
|
||||
as default_text_direction_test;
|
||||
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
|
||||
|
||||
Future<void> main() async {
|
||||
@@ -17,4 +19,5 @@ Future<void> runIntegration1OnMobile() async {
|
||||
anonymous_sign_in_test.main();
|
||||
create_new_page_test.main();
|
||||
document_test_runner.main();
|
||||
default_text_direction_test.main();
|
||||
}
|
||||
@@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapDown(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
bool pumpAndSettle = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
final location = getCenter(finder);
|
||||
final TestGesture gesture = await startGesture(
|
||||
location,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
kind: kind,
|
||||
);
|
||||
await gesture.cancel();
|
||||
await gesture.down(location);
|
||||
await gesture.cancel();
|
||||
if (pumpAndSettle) {
|
||||
await this.pumpAndSettle(
|
||||
Duration(milliseconds: milliseconds),
|
||||
EnginePhase.sendSemanticsUpdate,
|
||||
const Duration(seconds: 15),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithName(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
|
||||
@@ -8,18 +8,24 @@ import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
@@ -449,11 +455,8 @@ extension CommonOperations on WidgetTester {
|
||||
|
||||
// open the page after created
|
||||
if (openAfterCreated) {
|
||||
await openPage(
|
||||
// if the name is null, use the default name
|
||||
pageName ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout: layout,
|
||||
);
|
||||
// if the name is null, use empty string
|
||||
await openPage(pageName ?? '', layout: layout);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
@@ -598,6 +601,23 @@ extension CommonOperations on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> reorderFavorite({
|
||||
required String fromName,
|
||||
required String toName,
|
||||
}) async {
|
||||
final from = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.text(fromName),
|
||||
),
|
||||
to = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.text(toName),
|
||||
);
|
||||
final distanceY = getCenter(to).dy - getCenter(from).dx;
|
||||
await drag(from, Offset(0, distanceY));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
// tap the button with [FlowySvgData]
|
||||
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
@@ -609,9 +629,9 @@ extension CommonOperations on WidgetTester {
|
||||
// update the page icon in the sidebar
|
||||
Future<void> updatePageIconInSidebarByName({
|
||||
required String name,
|
||||
required String parentName,
|
||||
String? parentName,
|
||||
required ViewLayoutPB layout,
|
||||
required String icon,
|
||||
required EmojiIconData icon,
|
||||
}) async {
|
||||
final iconButton = find.descendant(
|
||||
of: findPageName(
|
||||
@@ -623,7 +643,11 @@ extension CommonOperations on WidgetTester {
|
||||
find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()),
|
||||
);
|
||||
await tapButton(iconButton);
|
||||
await tapEmoji(icon);
|
||||
if (icon.type == FlowyIconType.emoji) {
|
||||
await tapEmoji(icon.emoji);
|
||||
} else if (icon.type == FlowyIconType.icon) {
|
||||
await tapIcon(icon);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -631,7 +655,7 @@ extension CommonOperations on WidgetTester {
|
||||
Future<void> updatePageIconInTitleBarByName({
|
||||
required String name,
|
||||
required ViewLayoutPB layout,
|
||||
required String icon,
|
||||
required EmojiIconData icon,
|
||||
}) async {
|
||||
await openPage(
|
||||
name,
|
||||
@@ -643,7 +667,11 @@ extension CommonOperations on WidgetTester {
|
||||
);
|
||||
await tapButton(title);
|
||||
await tapButton(find.byType(EmojiPickerButton));
|
||||
await tapEmoji(icon);
|
||||
if (icon.type == FlowyIconType.emoji) {
|
||||
await tapEmoji(icon.emoji);
|
||||
} else if (icon.type == FlowyIconType.icon) {
|
||||
await tapIcon(icon);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -841,6 +869,70 @@ extension CommonOperations on WidgetTester {
|
||||
await tapButton(toggleHeading1);
|
||||
await pumpUntilNotFound(addMenuItem);
|
||||
}
|
||||
|
||||
/// Click the column menu button in the simple table
|
||||
Future<void> clickColumnMenuButton(int index) async {
|
||||
final columnMenuButton = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is SimpleTableMobileReorderButton &&
|
||||
w.index == index &&
|
||||
w.type == SimpleTableMoreActionType.column,
|
||||
);
|
||||
await tapButton(columnMenuButton);
|
||||
await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));
|
||||
}
|
||||
|
||||
/// Click the row menu button in the simple table
|
||||
Future<void> clickRowMenuButton(int index) async {
|
||||
final rowMenuButton = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is SimpleTableMobileReorderButton &&
|
||||
w.index == index &&
|
||||
w.type == SimpleTableMoreActionType.row,
|
||||
);
|
||||
await tapButton(rowMenuButton);
|
||||
await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));
|
||||
}
|
||||
|
||||
/// Click the SimpleTableQuickAction
|
||||
Future<void> clickSimpleTableQuickAction(SimpleTableMoreAction action) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is SimpleTableQuickAction && widget.type == action,
|
||||
);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Click the SimpleTableContentAction
|
||||
Future<void> clickSimpleTableBoldContentAction() async {
|
||||
final button = find.byType(SimpleTableContentBoldAction);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Cancel the table action menu
|
||||
Future<void> cancelTableActionMenu() async {
|
||||
final finder = find.byType(SimpleTableCellBottomSheet);
|
||||
if (finder.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tapAt(Offset.zero);
|
||||
await pumpUntilNotFound(finder);
|
||||
}
|
||||
|
||||
/// load icon list and return the first one
|
||||
Future<EmojiIconData> loadIcon() async {
|
||||
await loadIconGroups();
|
||||
final groups = kIconGroups!;
|
||||
final firstGroup = groups.first;
|
||||
final firstIcon = firstGroup.icons.first;
|
||||
return EmojiIconData.icon(
|
||||
IconsData(
|
||||
firstGroup.name,
|
||||
firstIcon.name,
|
||||
builtInSpaceColors.first,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsFinder on CommonFinders {
|
||||
|
||||
@@ -567,6 +567,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(phantom is PhantomChecklistItem, true);
|
||||
}
|
||||
|
||||
void assertPhantomChecklistItemContent(String content) {
|
||||
final phantom = find.byType(PhantomChecklistItem);
|
||||
final text = find.text(content);
|
||||
expect(find.descendant(of: phantom, matching: text), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -11,4 +20,43 @@ extension EmojiTestExtension on WidgetTester {
|
||||
);
|
||||
await tapButton(emojiWidget);
|
||||
}
|
||||
|
||||
Future<void> tapIcon(EmojiIconData icon) async {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
|
||||
final pickTab = find.byType(PickerTab);
|
||||
expect(pickTab, findsOneWidget);
|
||||
await pumpAndSettle();
|
||||
final iconTab = find.descendant(
|
||||
of: pickTab,
|
||||
matching: find.text(PickerTabType.icon.tr),
|
||||
);
|
||||
expect(iconTab, findsOneWidget);
|
||||
await tapButton(iconTab);
|
||||
final selectedSvg = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(selectedSvg.first);
|
||||
final colorPicker = find.byType(IconColorPicker);
|
||||
expect(colorPicker, findsOneWidget);
|
||||
final selectedColor = find.descendant(
|
||||
of: colorPicker,
|
||||
matching: find.byWidgetPredicate((w) {
|
||||
if (w is Container) {
|
||||
final d = w.decoration;
|
||||
if (d is ShapeDecoration) {
|
||||
if (d.color ==
|
||||
Color(int.parse(iconsData.color ?? builtInSpaceColors.first))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
await tapButton(selectedColor);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
@@ -5,6 +7,8 @@ import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
@@ -14,6 +18,7 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.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_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
@@ -117,7 +122,7 @@ extension Expectation on WidgetTester {
|
||||
return;
|
||||
}
|
||||
final iconWidget = find.byWidgetPredicate(
|
||||
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
|
||||
(widget) => widget is EmojiIconWidget && widget.emoji.emoji == emoji,
|
||||
);
|
||||
expect(iconWidget, findsOneWidget);
|
||||
}
|
||||
@@ -223,24 +228,52 @@ extension Expectation on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) {
|
||||
void expectViewHasIcon(String name, ViewLayoutPB layout, EmojiIconData data) {
|
||||
final pageName = findPageName(
|
||||
name,
|
||||
layout: layout,
|
||||
);
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.text(emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
final type = data.type;
|
||||
if (type == FlowyIconType.emoji) {
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.text(data.emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.icon) {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) {
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
void expectViewTitleHasIcon(
|
||||
String name,
|
||||
ViewLayoutPB layout,
|
||||
EmojiIconData data,
|
||||
) {
|
||||
final type = data.type;
|
||||
if (type == FlowyIconType.emoji) {
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(data.emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.icon) {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void expectSelectedReminder(ReminderOption option) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/ai/error.dart';
|
||||
import 'package:appflowy/ai/openai_client.dart';
|
||||
import 'package:appflowy/ai/text_completion.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ PODS:
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
@@ -112,6 +114,7 @@ DEPENDENCIES:
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -168,6 +171,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
@@ -185,21 +190,22 @@ SPEC CHECKSUMS:
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -50,6 +50,8 @@
|
||||
<string>AppFlowy needs access to your photos to let you add images to your documents</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>AppFlowy needs access to your camera to let you add images to your documents from camera</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -65,9 +67,9 @@
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -8,5 +8,12 @@
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:appflowy.com</string>
|
||||
<string>applinks:appflowy.io</string>
|
||||
<string>applinks:test.appflowy.com</string>
|
||||
<string>applinks:test.appflowy.io</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
+5
-3
@@ -1,8 +1,9 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import 'error.dart';
|
||||
import 'text_completion.dart';
|
||||
|
||||
abstract class AIRepository {
|
||||
Future<void> getStreamedCompletions({
|
||||
required String prompt,
|
||||
@@ -17,6 +18,7 @@ abstract class AIRepository {
|
||||
});
|
||||
|
||||
Future<void> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
+18
-9
@@ -3,16 +3,17 @@ import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ask_ai_action.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart' as fixnum;
|
||||
|
||||
import 'ai_client.dart';
|
||||
import 'error.dart';
|
||||
import 'text_completion.dart';
|
||||
|
||||
class AppFlowyAIService implements AIRepository {
|
||||
@override
|
||||
Future<FlowyResult<List<String>, AIError>> generateImage({
|
||||
@@ -39,6 +40,7 @@ class AppFlowyAIService implements AIRepository {
|
||||
|
||||
@override
|
||||
Future<CompletionStream> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
@@ -52,10 +54,17 @@ class AppFlowyAIService implements AIRepository {
|
||||
onEnd,
|
||||
onError,
|
||||
);
|
||||
final List<String> ragIds = [];
|
||||
if (objectId != null) {
|
||||
ragIds.add(objectId);
|
||||
}
|
||||
|
||||
final payload = CompleteTextPB(
|
||||
text: text,
|
||||
completionType: completionType,
|
||||
streamPort: fixnum.Int64(stream.nativePort),
|
||||
objectId: objectId ?? "",
|
||||
ragIds: ragIds,
|
||||
);
|
||||
|
||||
// ignore: unawaited_futures
|
||||
@@ -64,15 +73,15 @@ class AppFlowyAIService implements AIRepository {
|
||||
}
|
||||
}
|
||||
|
||||
CompletionTypePB completionTypeFromInt(SmartEditAction action) {
|
||||
CompletionTypePB completionTypeFromInt(AskAIAction action) {
|
||||
switch (action) {
|
||||
case SmartEditAction.summarize:
|
||||
case AskAIAction.summarize:
|
||||
return CompletionTypePB.MakeShorter;
|
||||
case SmartEditAction.fixSpelling:
|
||||
case AskAIAction.fixSpelling:
|
||||
return CompletionTypePB.SpellingAndGrammar;
|
||||
case SmartEditAction.improveWriting:
|
||||
case AskAIAction.improveWriting:
|
||||
return CompletionTypePB.ImproveWriting;
|
||||
case SmartEditAction.makeItLonger:
|
||||
case AskAIAction.makeItLonger:
|
||||
return CompletionTypePB.MakeLonger;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+2
-1
@@ -1,11 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'ai_client.dart';
|
||||
import 'error.dart';
|
||||
import 'text_completion.dart';
|
||||
|
||||
@@ -160,6 +160,7 @@ class HttpOpenAIRepository implements AIRepository {
|
||||
|
||||
@override
|
||||
Future<void> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'text_completion.freezed.dart';
|
||||
part 'text_completion.g.dart';
|
||||
|
||||
@@ -49,6 +49,7 @@ class KVKeys {
|
||||
/// {'SidebarFolderCategoryType.value': true}
|
||||
static const String expandedFolders = 'expandedFolders';
|
||||
|
||||
/// @deprecated in version 0.7.6
|
||||
/// The key for saving if showing the rename dialog when creating a new file
|
||||
///
|
||||
/// The value is a boolean string.
|
||||
@@ -57,6 +58,8 @@ class KVKeys {
|
||||
|
||||
static const String kCloudType = 'kCloudType';
|
||||
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
|
||||
static const String kAppFlowyBaseShareDomain = 'kAppFlowyBaseShareDomain';
|
||||
static const String kAppFlowyEnableSyncTrace = 'kAppFlowyEnableSyncTrace';
|
||||
|
||||
/// The key for saving the text scale factor.
|
||||
///
|
||||
@@ -107,4 +110,9 @@ class KVKeys {
|
||||
///
|
||||
/// The value is a boolean string
|
||||
static const String hasUpgradedSpace = 'hasUpgradedSpace060';
|
||||
|
||||
/// The key for saving the recent icons
|
||||
///
|
||||
/// The value is a json string of [RecentIcons]
|
||||
static const String recentIcons = 'kRecentIcons';
|
||||
}
|
||||
@@ -1,16 +1,24 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as launcher;
|
||||
|
||||
typedef OnFailureCallback = void Function(Uri uri);
|
||||
|
||||
Future<bool> afLaunchUrl(
|
||||
/// Launch the uri
|
||||
///
|
||||
/// If the uri is a local file path, it will be opened with the OpenFilex.
|
||||
/// Otherwise, it will be launched with the url_launcher.
|
||||
Future<bool> afLaunchUri(
|
||||
Uri uri, {
|
||||
BuildContext? context,
|
||||
OnFailureCallback? onFailure,
|
||||
@@ -18,6 +26,18 @@ Future<bool> afLaunchUrl(
|
||||
String? webOnlyWindowName,
|
||||
bool addingHttpSchemeWhenFailed = false,
|
||||
}) async {
|
||||
final url = uri.toString();
|
||||
final decodedUrl = Uri.decodeComponent(url);
|
||||
|
||||
// check if the uri is the local file path
|
||||
if (localPathRegex.hasMatch(decodedUrl)) {
|
||||
return _afLaunchLocalUri(
|
||||
uri,
|
||||
context: context,
|
||||
onFailure: onFailure,
|
||||
);
|
||||
}
|
||||
|
||||
// try to launch the uri directly
|
||||
bool result;
|
||||
try {
|
||||
@@ -32,7 +52,7 @@ Future<bool> afLaunchUrl(
|
||||
}
|
||||
|
||||
// if the uri is not a valid url, try to launch it with http scheme
|
||||
final url = uri.toString();
|
||||
|
||||
if (addingHttpSchemeWhenFailed &&
|
||||
!result &&
|
||||
!isURL(url, {'require_protocol': true})) {
|
||||
@@ -54,9 +74,14 @@ Future<bool> afLaunchUrl(
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Launch the url string
|
||||
///
|
||||
/// See [afLaunchUri] for more details.
|
||||
Future<bool> afLaunchUrlString(
|
||||
String url, {
|
||||
bool addingHttpSchemeWhenFailed = false,
|
||||
BuildContext? context,
|
||||
OnFailureCallback? onFailure,
|
||||
}) async {
|
||||
final Uri uri;
|
||||
try {
|
||||
@@ -67,12 +92,56 @@ Future<bool> afLaunchUrlString(
|
||||
}
|
||||
|
||||
// try to launch the uri directly
|
||||
return afLaunchUrl(
|
||||
return afLaunchUri(
|
||||
uri,
|
||||
addingHttpSchemeWhenFailed: addingHttpSchemeWhenFailed,
|
||||
context: context,
|
||||
onFailure: onFailure,
|
||||
);
|
||||
}
|
||||
|
||||
/// Launch the local uri
|
||||
///
|
||||
/// See [afLaunchUri] for more details.
|
||||
Future<bool> _afLaunchLocalUri(
|
||||
Uri uri, {
|
||||
BuildContext? context,
|
||||
OnFailureCallback? onFailure,
|
||||
}) async {
|
||||
final decodedUrl = Uri.decodeComponent(uri.toString());
|
||||
// open the file with the OpenfileX
|
||||
var result = await OpenFilex.open(decodedUrl);
|
||||
if (result.type != ResultType.done) {
|
||||
// For the file cant be opened, fallback to open the folder
|
||||
final parentFolder = Directory(decodedUrl).parent.path;
|
||||
result = await OpenFilex.open(parentFolder);
|
||||
}
|
||||
// show the toast if the file is not found
|
||||
final message = switch (result.type) {
|
||||
ResultType.done => LocaleKeys.openFileMessage_success.tr(),
|
||||
ResultType.fileNotFound => LocaleKeys.openFileMessage_fileNotFound.tr(),
|
||||
ResultType.noAppToOpen => LocaleKeys.openFileMessage_noAppToOpenFile.tr(),
|
||||
ResultType.permissionDenied =>
|
||||
LocaleKeys.openFileMessage_permissionDenied.tr(),
|
||||
ResultType.error => LocaleKeys.failedToOpenUrl.tr(),
|
||||
};
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: message,
|
||||
type: result.type == ResultType.done
|
||||
? ToastificationType.success
|
||||
: ToastificationType.error,
|
||||
);
|
||||
}
|
||||
final openFileSuccess = result.type == ResultType.done;
|
||||
if (!openFileSuccess && onFailure != null) {
|
||||
onFailure(uri);
|
||||
Log.error('Failed to open file: $result.message');
|
||||
}
|
||||
return openFileSuccess;
|
||||
}
|
||||
|
||||
void _errorHandler(
|
||||
Uri uri, {
|
||||
BuildContext? context,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'backend_env.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@@ -39,6 +41,8 @@ class AppFlowyCloudConfiguration {
|
||||
required this.base_url,
|
||||
required this.ws_base_url,
|
||||
required this.gotrue_url,
|
||||
required this.enable_sync_trace,
|
||||
required this.base_web_domain,
|
||||
});
|
||||
|
||||
factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -47,6 +51,14 @@ class AppFlowyCloudConfiguration {
|
||||
final String base_url;
|
||||
final String ws_base_url;
|
||||
final String gotrue_url;
|
||||
final bool enable_sync_trace;
|
||||
|
||||
/// The base domain is used in
|
||||
///
|
||||
/// - Share URL
|
||||
/// - Publish URL
|
||||
/// - Copy Link To Block
|
||||
final String base_web_domain;
|
||||
|
||||
Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);
|
||||
|
||||
@@ -55,6 +67,8 @@ class AppFlowyCloudConfiguration {
|
||||
base_url: '',
|
||||
ws_base_url: '',
|
||||
gotrue_url: '',
|
||||
enable_sync_trace: false,
|
||||
base_web_domain: ShareConstants.defaultBaseWebDomain,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+48
-1
@@ -2,6 +2,7 @@ import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/env/backend_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
|
||||
@@ -155,6 +156,13 @@ Future<void> _setAppFlowyCloudUrl(String? url) async {
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, url ?? '');
|
||||
}
|
||||
|
||||
Future<void> useBaseWebDomain(String? url) async {
|
||||
await getIt<KeyValueStorage>().set(
|
||||
KVKeys.kAppFlowyBaseShareDomain,
|
||||
url ?? ShareConstants.defaultBaseWebDomain,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
|
||||
await _setAppFlowyCloudUrl(url);
|
||||
@@ -212,6 +220,8 @@ class AppFlowyCloudSharedEnv {
|
||||
base_url: Env.afCloudUrl,
|
||||
ws_base_url: await _getAppFlowyCloudWSUrl(Env.afCloudUrl),
|
||||
gotrue_url: await _getAppFlowyCloudGotrueUrl(Env.afCloudUrl),
|
||||
enable_sync_trace: false,
|
||||
base_web_domain: Env.baseWebDomain,
|
||||
);
|
||||
|
||||
return AppFlowyCloudSharedEnv(
|
||||
@@ -232,6 +242,7 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
|
||||
Uri baseUri,
|
||||
String baseUrl,
|
||||
AuthenticatorType authenticatorType,
|
||||
String baseShareDomain,
|
||||
) async {
|
||||
// In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports.
|
||||
// This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server
|
||||
@@ -241,12 +252,18 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
|
||||
base_url: "$baseUrl:8000",
|
||||
ws_base_url: "ws://${baseUri.host}:8000/ws/v1",
|
||||
gotrue_url: "$baseUrl:9999",
|
||||
enable_sync_trace: true,
|
||||
base_web_domain: ShareConstants.testBaseWebDomain,
|
||||
);
|
||||
} else {
|
||||
return AppFlowyCloudConfiguration(
|
||||
base_url: baseUrl,
|
||||
ws_base_url: await _getAppFlowyCloudWSUrl(baseUrl),
|
||||
gotrue_url: await _getAppFlowyCloudGotrueUrl(baseUrl),
|
||||
enable_sync_trace: await getSyncLogEnabled(),
|
||||
base_web_domain: authenticatorType == AuthenticatorType.appflowyCloud
|
||||
? ShareConstants.defaultBaseWebDomain
|
||||
: baseShareDomain,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -255,10 +272,16 @@ Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig(
|
||||
AuthenticatorType authenticatorType,
|
||||
) async {
|
||||
final baseURL = await getAppFlowyCloudUrl();
|
||||
final baseShareDomain = await getAppFlowyShareDomain();
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(baseURL);
|
||||
return await configurationFromUri(uri, baseURL, authenticatorType);
|
||||
return await configurationFromUri(
|
||||
uri,
|
||||
baseURL,
|
||||
authenticatorType,
|
||||
baseShareDomain,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse AppFlowy Cloud URL: $e");
|
||||
return AppFlowyCloudConfiguration.defaultConfig();
|
||||
@@ -271,6 +294,30 @@ Future<String> getAppFlowyCloudUrl() async {
|
||||
return result ?? kAppflowyCloudUrl;
|
||||
}
|
||||
|
||||
Future<String> getAppFlowyShareDomain() async {
|
||||
final result =
|
||||
await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyBaseShareDomain);
|
||||
return result ?? ShareConstants.defaultBaseWebDomain;
|
||||
}
|
||||
|
||||
Future<bool> getSyncLogEnabled() async {
|
||||
final result =
|
||||
await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyEnableSyncTrace);
|
||||
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.toLowerCase() == "true";
|
||||
}
|
||||
|
||||
Future<void> setSyncLogEnabled(bool enable) async {
|
||||
await getIt<KeyValueStorage>().set(
|
||||
KVKeys.kAppFlowyEnableSyncTrace,
|
||||
enable.toString().toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
|
||||
try {
|
||||
final uri = Uri.parse(baseURL);
|
||||
|
||||
+8
@@ -1,5 +1,6 @@
|
||||
// lib/env/env.dart
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:envied/envied.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
@@ -43,4 +44,11 @@ abstract class Env {
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String sentryDsn = _Env.sentryDsn;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: false,
|
||||
varName: 'BASE_WEB_DOMAIN',
|
||||
defaultValue: ShareConstants.defaultBaseWebDomain,
|
||||
)
|
||||
static const String baseWebDomain = _Env.baseWebDomain;
|
||||
}
|
||||
+7
-6
@@ -6,7 +6,6 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -26,11 +25,13 @@ class DocumentPageStyleBloc
|
||||
if (view.id.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final layoutObject =
|
||||
await ViewBackendService.getView(view.id).fold(
|
||||
(s) => jsonDecode(s.extra),
|
||||
(f) => {},
|
||||
);
|
||||
Map layoutObject = {};
|
||||
final data = await ViewBackendService.getView(view.id);
|
||||
data.onSuccess((s) {
|
||||
if (s.extra.isNotEmpty) {
|
||||
layoutObject = jsonDecode(s.extra);
|
||||
}
|
||||
});
|
||||
final fontLayout = _getSelectedFontLayout(layoutObject);
|
||||
final lineHeightLayout = _getSelectedLineHeightLayout(
|
||||
layoutObject,
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
|
||||
part 'recent_view_bloc.freezed.dart';
|
||||
|
||||
class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
@@ -39,7 +41,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
add(
|
||||
RecentViewEvent.updateNameOrIcon(
|
||||
view.name,
|
||||
view.icon.value,
|
||||
view.icon.toEmojiIconData(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -61,7 +63,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
name: view.name,
|
||||
icon: view.icon.value,
|
||||
icon: view.icon.toEmojiIconData(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -72,7 +74,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
name: view.name,
|
||||
icon: view.icon.value,
|
||||
icon: view.icon.toEmojiIconData(),
|
||||
coverTypeV2: cover.type,
|
||||
coverValue: cover.value,
|
||||
),
|
||||
@@ -82,7 +84,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
name: view.name,
|
||||
icon: view.icon.value,
|
||||
icon: view.icon.toEmojiIconData(),
|
||||
coverTypeV1: coverTypeV1,
|
||||
coverValue: coverValue,
|
||||
),
|
||||
@@ -135,14 +137,17 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
@freezed
|
||||
class RecentViewEvent with _$RecentViewEvent {
|
||||
const factory RecentViewEvent.initial() = Initial;
|
||||
|
||||
const factory RecentViewEvent.updateCover(
|
||||
CoverType coverTypeV1, // for the version under 0.5.5, including 0.5.5
|
||||
CoverType coverTypeV1,
|
||||
// for the version under 0.5.5, including 0.5.5
|
||||
PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5
|
||||
String? coverValue,
|
||||
) = UpdateCover;
|
||||
|
||||
const factory RecentViewEvent.updateNameOrIcon(
|
||||
String name,
|
||||
String icon,
|
||||
EmojiIconData icon,
|
||||
) = UpdateNameOrIcon;
|
||||
}
|
||||
|
||||
@@ -150,12 +155,12 @@ class RecentViewEvent with _$RecentViewEvent {
|
||||
class RecentViewState with _$RecentViewState {
|
||||
const factory RecentViewState({
|
||||
required String name,
|
||||
required String icon,
|
||||
required EmojiIconData icon,
|
||||
@Default(CoverType.none) CoverType coverTypeV1,
|
||||
PageStyleCoverImageType? coverTypeV2,
|
||||
@Default(null) String? coverValue,
|
||||
}) = _RecentViewState;
|
||||
|
||||
factory RecentViewState.initial() =>
|
||||
const RecentViewState(name: '', icon: '');
|
||||
RecentViewState(name: '', icon: EmojiIconData.none());
|
||||
}
|
||||
@@ -7,11 +7,14 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -105,6 +108,12 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
create: (_) =>
|
||||
ShareBloc(view: view)..add(const ShareEvent.initial()),
|
||||
),
|
||||
if (state.userProfilePB != null)
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
UserWorkspaceBloc(userProfile: state.userProfilePB!)
|
||||
..add(const UserWorkspaceEvent.initial()),
|
||||
),
|
||||
if (view.layout.isDocumentView)
|
||||
BlocProvider(
|
||||
create: (_) => DocumentPageStyleBloc(view: view)
|
||||
@@ -255,15 +264,14 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
}
|
||||
|
||||
Widget _buildTitle(BuildContext context, ViewPB? view) {
|
||||
final icon = view?.icon.value;
|
||||
final icon = view?.icon;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null && icon.isNotEmpty) ...[
|
||||
FlowyText.emoji(
|
||||
icon,
|
||||
fontSize: 15.0,
|
||||
figmaLineHeight: 18.0,
|
||||
if (icon != null && icon.value.isNotEmpty) ...[
|
||||
RawEmojiIconWidget(
|
||||
emoji: icon.toEmojiIconData(),
|
||||
emojiSize: 15,
|
||||
),
|
||||
const HSpace(4),
|
||||
],
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||
final url = context.read<ShareBloc>().state.url;
|
||||
if (url.isNotEmpty) {
|
||||
unawaited(
|
||||
afLaunchUrl(
|
||||
afLaunchUri(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ class BottomSheetBackButton extends StatelessWidget {
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_app_bar_back_s,
|
||||
FlowySvgs.m_bottom_sheet_back_s,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-5
@@ -138,7 +138,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
),
|
||||
_divider(),
|
||||
..._buildPublishActions(context),
|
||||
_divider(),
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
@@ -205,8 +204,5 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _divider() => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
);
|
||||
Widget _divider() => const MobileQuickActionDivider();
|
||||
}
|
||||
+29
-13
@@ -57,6 +57,7 @@ Future<T?> showMobileBottomSheet<T>(
|
||||
double maxChildSize = 0.8,
|
||||
double initialChildSize = 0.51,
|
||||
double bottomSheetPadding = 0,
|
||||
bool enablePadding = true,
|
||||
}) async {
|
||||
assert(
|
||||
showHeader ||
|
||||
@@ -172,14 +173,18 @@ Future<T?> showMobileBottomSheet<T>(
|
||||
}
|
||||
|
||||
// ----- content area -----
|
||||
// add content padding and extra bottom padding
|
||||
children.add(
|
||||
Padding(
|
||||
padding:
|
||||
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
if (enablePadding) {
|
||||
// add content padding and extra bottom padding
|
||||
children.add(
|
||||
Padding(
|
||||
padding:
|
||||
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(child);
|
||||
}
|
||||
// ----- content area -----
|
||||
|
||||
if (children.length == 1) {
|
||||
@@ -211,14 +216,21 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
required this.showDoneButton,
|
||||
this.onRemove,
|
||||
this.onDone,
|
||||
this.onBack,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final String title;
|
||||
|
||||
final bool showBackButton;
|
||||
final bool showCloseButton;
|
||||
final bool showRemoveButton;
|
||||
final String title;
|
||||
final bool showDoneButton;
|
||||
|
||||
final VoidCallback? onRemove;
|
||||
final VoidCallback? onBack;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
final void Function(BuildContext context)? onDone;
|
||||
|
||||
@override
|
||||
@@ -230,14 +242,18 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
if (showBackButton)
|
||||
const Align(
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: BottomSheetBackButton(),
|
||||
child: BottomSheetBackButton(
|
||||
onTap: onBack,
|
||||
),
|
||||
),
|
||||
if (showCloseButton)
|
||||
const Align(
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: BottomSheetCloseButton(),
|
||||
child: BottomSheetCloseButton(
|
||||
onTap: onClose,
|
||||
),
|
||||
),
|
||||
if (showRemoveButton)
|
||||
Align(
|
||||
|
||||
+2
-6
@@ -7,7 +7,6 @@ import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -113,12 +112,8 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
context,
|
||||
showDragHandle: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (_) => SeparatedColumn(
|
||||
builder: (_) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
separatorBuilder: () => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
),
|
||||
children: [
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.board_column_renameColumn.tr(),
|
||||
@@ -132,6 +127,7 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
const MobileQuickActionDivider(),
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.board_column_hideColumn.tr(),
|
||||
icon: FlowySvgs.hide_s,
|
||||
|
||||
+2
-3
@@ -149,7 +149,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.duplicate_s,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
const MobileQuickActionDivider(),
|
||||
MobileQuickActionButton(
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
@@ -201,7 +201,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.add_cover_s,
|
||||
text: 'Add cover',
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
const MobileQuickActionDivider(),
|
||||
MobileQuickActionButton(
|
||||
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
@@ -209,7 +209,6 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.trash_s,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+4
-2
@@ -9,10 +9,10 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -863,6 +863,8 @@ class _SelectOptionListState extends State<_SelectOptionList> {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
// disable the inner scroll physics, so the outer ListView can scroll
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: widget.selectOptions
|
||||
.mapIndexed(
|
||||
(index, option) => _SelectOptionTile(
|
||||
|
||||
+2
-5
@@ -48,7 +48,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
context.pop();
|
||||
}
|
||||
}),
|
||||
_divider(),
|
||||
const MobileQuickActionDivider(),
|
||||
_actionButton(
|
||||
context,
|
||||
_Action.duplicate,
|
||||
@@ -58,7 +58,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
},
|
||||
!isInline,
|
||||
),
|
||||
_divider(),
|
||||
const MobileQuickActionDivider(),
|
||||
_actionButton(
|
||||
context,
|
||||
_Action.delete,
|
||||
@@ -68,7 +68,6 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
},
|
||||
!isInline,
|
||||
),
|
||||
_divider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -88,8 +87,6 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
enable: enable,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() => const Divider(height: 8.5, thickness: 0.5);
|
||||
}
|
||||
|
||||
enum _Action {
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/built_in_svgs.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
@@ -228,12 +229,13 @@ class _UserIcon extends StatelessWidget {
|
||||
fontSize: 26,
|
||||
),
|
||||
onTap: () async {
|
||||
final icon = await context.push<EmojiPickerResult>(
|
||||
final icon = await context.push<EmojiIconData>(
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.pageTitle:
|
||||
LocaleKeys.titleBar_userIcon.tr(),
|
||||
MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name],
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
|
||||
Loaded 100 of 3356 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user