Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63f767926 | ||
|
|
8826e479eb | ||
|
|
8e4fe3d559 | ||
|
|
e2ee11e48a | ||
|
|
bdc0fa1f2a | ||
|
|
3836545682 | ||
|
|
02eb0e0b83 |
No files matched your search
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -18,8 +19,8 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
FLUTTER_VERSION: "3.22.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
pull_request:
|
||||
@@ -15,11 +16,12 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
@@ -6,8 +6,8 @@ on:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -338,7 +338,7 @@ jobs:
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-22.04,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
flutter_profile: production-linux-x86_64,
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CLOUD_VERSION: 0.8.3-amd64
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
ubuntu-job:
|
||||
|
||||
@@ -10,8 +10,8 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
name: Tauri-CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/tauri2_ci.yaml"
|
||||
- "frontend/rust-lib/**"
|
||||
paths-ignore:
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
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-ubuntu:
|
||||
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"
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Tauri-CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build/tauri
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
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 --locked 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 --locked 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"
|
||||
@@ -0,0 +1,152 @@
|
||||
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.80.1"
|
||||
|
||||
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 --locked 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 --locked 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
|
||||
@@ -0,0 +1,75 @@
|
||||
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
|
||||
@@ -0,0 +1,65 @@
|
||||
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
|
||||
|
||||
-119
@@ -1,123 +1,4 @@
|
||||
# Release Notes
|
||||
## Version 0.8.9 - 16/04/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Supported pasting a link as a mention, providing a more condensed visualization of linked content
|
||||
- Supported converting between link formats (e.g. transforming a mention into a bookmark)
|
||||
- Improved the link editing experience with enhanced UX
|
||||
- Added OTP (One-Time Password) support for sign-in authentication
|
||||
- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet
|
||||
#### Bug Fixes
|
||||
- Fixed an issue where properties were not displaying in the row detail page
|
||||
- Fixed a bug where Undo didn't work in the row detail page
|
||||
- Fixed an issue where blocks didn't grow when the grid got bigger
|
||||
- Fixed several bugs related to AI writers
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Added sign-in with OTP (One-Time Password)
|
||||
#### Bug Fixes
|
||||
- Fixed an issue where the slash menu sometimes failed to display
|
||||
- Updated the mention page block to handle page selection with more context.
|
||||
|
||||
## Version 0.8.8 - 01/04/2025
|
||||
### New Features
|
||||
- Added support for selecting AI models in AI writer
|
||||
- Revamped link menu in toolbar
|
||||
- Added support for using ":" to add emojis in documents
|
||||
- Passed the history of past AI prompts and responses to AI writer
|
||||
### Bug Fixes
|
||||
- Improved AI writer scrolling user experience
|
||||
- Fixed issue where checklist items would disappear during reordering
|
||||
- Fixed numbered lists generated by AI to maintain the same index as the input
|
||||
|
||||
## Version 0.8.7 - 18/03/2025
|
||||
### New Features
|
||||
- Made local AI free and integrated with Ollama
|
||||
- Supported nested lists within callout and quote blocks
|
||||
- Revamped the document's floating toolbar and added Turn Into
|
||||
- Enabled custom icons in callout blocks
|
||||
### Bug Fixes
|
||||
- Fixed occasional incorrect positioning of the slash menu
|
||||
- Improved AI Chat and AI Writers with various bug fixes
|
||||
- Adjusted the columns block to match the width of the editor
|
||||
- Fixed a potential segfault caused by infinite recursion in the trash view
|
||||
- Resolved an issue where the first added cover might be invisible
|
||||
- Fixed adding cover images via Unsplash
|
||||
|
||||
## Version 0.8.6 - 06/03/2025
|
||||
### Bug Fixes
|
||||
- Fix the incorrect title positioning when adjusting the document width setting
|
||||
- Enhance the user experience of the icon color picker for smoother interactions
|
||||
- Add missing icons to the database to ensure completeness and consistency
|
||||
- Resolve the issue with links not functioning correctly on Linux systems
|
||||
- Improve the outline feature to work seamlessly within columns
|
||||
- Center the bulleted list icon within columns for better visual alignment
|
||||
- Enable dragging blocks under tables in the second column to enhance flexibility
|
||||
- Disable the AI writer feature within tables to prevent conflicts and improve usability
|
||||
- Automatically enable the header row when converting content from Markdown to ensure proper formatting
|
||||
- Use the "Undo" function to revert the auto-formatting
|
||||
|
||||
## Version 0.8.5 - 04/03/2025
|
||||
### New Features
|
||||
- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu
|
||||
- AI Writers: New AI assistants in documents with response formatting options (list, table, text with images, image-only), follow-up questions, contextual memory, and more
|
||||
- Compact Mode for Databases: Enable compact mode for grid and kanban views (full-page and inline) to increase information density, displaying more data per screen
|
||||
### Bug Fixes
|
||||
- Fixed an issue where callout blocks couldn’t be deleted when appearing as the first line in a document
|
||||
- Fixed a bug preventing the relation field in databases from opening
|
||||
- Fixed an issue where links in documents were unclickable on Linux
|
||||
|
||||
## Version 0.8.4 - 18/02/2025
|
||||
### New Features
|
||||
- Switch AI mode on mobile
|
||||
- Support locking page
|
||||
- Support uploading svg file as icon
|
||||
- Support the slash, at, and plus menus on mobile
|
||||
### Bug Fixes
|
||||
- Gallery not rendering in row page
|
||||
- Save image should not copy the image (mobile)
|
||||
- Support exporting more content to markdown
|
||||
|
||||
## Version 0.8.2 - 23/01/2025
|
||||
### New Features
|
||||
- Customized database view icons
|
||||
- Support for uploading images as custom icons
|
||||
- Enabled selecting multiple AI messages to save into a document
|
||||
- Added the ability to scale the app's display size on mobile
|
||||
- Support for pasting image links without file extensions
|
||||
### Bug Fixes
|
||||
- Fixed an issue where pasting tables from other apps wasn't working
|
||||
- Fixed homepage URL issues in Settings
|
||||
- Fixed an issue where the 'Cancel' button was not visible on the Shortcuts page
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<b>
|
||||
<a href="https://www.appflowy.com">AppFlowy</a><br>
|
||||
<a href="https://www.appflowy.io">AppFlowy.IO</a><br>
|
||||
</b>
|
||||
⭐️ The Open Source Alternative To Notion ⭐️ <br>
|
||||
</h1>
|
||||
@@ -18,18 +18,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.appflowy.com"><b>Website</b></a> •
|
||||
<a href="https://www.appflowy.io"><b>Website</b></a> •
|
||||
<a href="https://forum.appflowy.io/"><b>Forum</b></a> •
|
||||
<a href="https://discord.gg/9Q2xaN37tV"><b>Discord</b></a> •
|
||||
<a href="https://www.reddit.com/r/AppFlowy"><b>Reddit</b></a> •
|
||||
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
|
||||
<p align="center"><img src="https://appflowy.com/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
|
||||
<p align="center"><img src="https://appflowy.io/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
|
||||
|
||||
<br></br>
|
||||
<p align="center" >
|
||||
@@ -48,7 +48,7 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
|
||||
- [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://appflowy.com/docs/self-host-appflowy-overview)
|
||||
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
|
||||
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
|
||||
|
||||
## Built With
|
||||
@@ -78,7 +78,7 @@ report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labe
|
||||
|
||||
## **Releases**
|
||||
|
||||
Please see the [changelog](https://appflowy.com/what-is-new) for more details about a given release.
|
||||
Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -89,7 +89,9 @@ 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.
|
||||
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 🌎🗺
|
||||
|
||||
@@ -150,8 +152,8 @@ more information.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to these amazing projects which help power AppFlowy:
|
||||
Special thanks to these amazing projects which help power AppFlowy.IO:
|
||||
|
||||
- [cargo-make](https://github.com/sagiegurari/cargo-make)
|
||||
- [contrib.rocks](https://contrib.rocks)
|
||||
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
|
||||
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
|
||||
+1
-1
@@ -4,7 +4,7 @@ workflows:
|
||||
instance_type: mac_mini_m2
|
||||
max_build_duration: 30
|
||||
environment:
|
||||
flutter: 3.27.4
|
||||
flutter: 3.22.3
|
||||
xcode: latest
|
||||
cocoapods: default
|
||||
|
||||
|
||||
Vendored
+138
-123
@@ -1,125 +1,140 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
]
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
{
|
||||
// https://tauri.app/v1/guides/debugging/vs-code
|
||||
"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,6 +245,51 @@
|
||||
"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.9.0"
|
||||
APPFLOWY_VERSION = "0.7.9"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -4,7 +4,6 @@ analyzer:
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
- "**/*.freezed.dart"
|
||||
- "packages/**/*.dart"
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
||||
@@ -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 35
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -43,8 +43,6 @@
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||
-->
|
||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -1,874 +0,0 @@
|
||||
{
|
||||
"prompts": [
|
||||
{
|
||||
"id": "builtin_prompt_1",
|
||||
"name": "Linux terminal",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so.\nThis is my first command:\n\n[command]",
|
||||
"example": "ls -l",
|
||||
"sampleResponse": "```\ntotal 8\ndrwxr-xr-x 2 alice alice 4096 Apr 12 12:34 alice\ndrwxr-xr-x 3 bob bob 4096 Apr 12 12:35 bob\n```"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_2",
|
||||
"name": "Cover Letter Writer and Advisor",
|
||||
"category": "business",
|
||||
"content": "I want you to act as a career advisor, who can assist in crafting a compelling cover letter for job applications. Share insights on how to structure the letter, highlight relevant skills and experiences, and express enthusiasm for the role. Offer advice on how to tailor the letter to the specific job description, company culture, and industry standards. Provide tips on maintaining a professional tone, proofreading for errors, and making a memorable impression on hiring managers. My first request is \n\n[request]",
|
||||
"example": "Help me write a cover letter for a software engineer position at a tech startup, focusing on my coding skills, problem-solving ability, and experience with agile development methodologies."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_3",
|
||||
"name": "Business Plan Consultant",
|
||||
"category": "business",
|
||||
"content": "I want you to act as a business plan consultant, who can guide me through the process of creating a comprehensive and persuasive business plan for a new startup or existing business. Discuss key elements to include in the plan, such as executive summary, company overview, market analysis, marketing strategies, organizational structure, and financial projections. Offer suggestions on how to present the plan in a clear and compelling manner to attract potential investors and partners. My first request is:\n\n[request]",
|
||||
"example": "Help me outline a business plan for an online fitness coaching platform targeting busy professionals."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_4",
|
||||
"name": "Niche Identifier",
|
||||
"category": "business",
|
||||
"content": "As an online business consultant, your task is to help an aspiring entrepreneur identify 5 profitable niches for starting an online business. Consider factors such as market demand, competition, target audience, and potential for growth. For each niche, provide a brief description of the products or services that could be offered, the target customer demographics, and the potential marketing strategies to reach them. Suggest low-cost ways to validate the niche ideas and test their viability before investing significant time or money.",
|
||||
"example": "Help me create a month-long content calendar for Instagram focused on promoting a new skincare line."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_5",
|
||||
"name": "Chief Executive Officer",
|
||||
"category": "business",
|
||||
"content": "I want you to act as a CEO, who can provide strategic insights into running a successful organization. Share advice on setting the company's vision and mission, building a high-performing team, managing resources, and driving growth. Offer guidance on making tough decisions, dealing with risks and crises, and maintaining stakeholder relationships. Also, discuss the responsibilities of a CEO in ensuring ethical conduct and corporate social responsibility. My first request is:\n\n[request]",
|
||||
"example": "Provide a strategic plan for a startup tech company in its first year of operation, focusing on key objectives, potential challenges, and growth strategies."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_6",
|
||||
"name": "Accountant",
|
||||
"category": "business",
|
||||
"content": "I want you to act as an accountant and come up with creative ways to manage finances. You’ll need to consider budgeting, investment strategies and risk management when creating a financial plan for your client. In some cases, you may also need to provide advice on taxation laws and regulations in order to help them maximize their profits. My first suggestion request is:\n\n[request].",
|
||||
"example": "Create a financial plan for a small business that focuses on cost savings and long-term investments"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_7",
|
||||
"name": "Creative Branding Strategist",
|
||||
"category": "business",
|
||||
"content": "You are a creative branding strategist, specializing in helping small businesses establish a strong and memorable brand identity. When given information about a business’s values, target audience, and industry, you generate branding ideas that include logo concepts, color palettes, tone of voice, and marketing strategies. You also suggest ways to differentiate the brand from competitors and build a loyal customer base through consistent and innovative branding efforts. Reply in English using professional tone for everyone.",
|
||||
"example": "Help me create a branding strategy for a new organic skincare line targeting eco-conscious consumers."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_8",
|
||||
"name": "Emoji-fy",
|
||||
"category": "other",
|
||||
"content": "Your task is to take the following plain text message provided and convert it into an expressive, emoji-rich message that conveys the same meaning and intent. Replace key words and phrases with relevant emojis wherever possible to add visual interest and emotion. Use emojis creatively but ensure the message remains clear and easy to understand. Do not alter the core message or add new information.\n\n[message]",
|
||||
"sampleResponse": "🍎’s goal isn’t just to 💰. Our 🎯 is to 🎨 design, 🔧 develop, and 🚀 bring to market outstanding products... We trust that, as a result, people will ❤️ them, and as another bonus, we’ll earn some 💵. But we’re absolutely crystal clear about what our 🎯 are."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_9",
|
||||
"name": "Explain like I’m 5",
|
||||
"category": "learning",
|
||||
"content": "Explain [concept or question, e.g., What is gravity?, What does money do?, How do plants grow?] in a way that a 5-year-old would understand. Use very simple language, short sentences, and relatable examples from everyday life (like toys, animals, food, or playground activities). Avoid technical jargon and aim to make the explanation fun and clear.\n\nIf helpful, include a quick story or analogy to illustrate the idea. You can end the explanation with a cheerful summary or a fun fact to keep it engaging.",
|
||||
"sampleResponse": "The internet is like a big library where you can find lots of books, pictures, and videos. But instead of going to the library, you can sit at home and look at everything on a computer or tablet. It’s like magic!"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_10",
|
||||
"name": "Social Media Master",
|
||||
"category": "marketing",
|
||||
"content": "Create an engaging and upbeat social media post for [platform, e.g., Instagram/Twitter/LinkedIn] about [topic or event, e.g., product launch, personal milestone, motivational message]. The tone should be [desired tone, e.g., energetic, friendly, professional] and include [specific elements, e.g., a call to action, hashtags, emojis]. Make it appealing to [target audience, e.g., young professionals, fitness enthusiasts, small business owners].",
|
||||
"sampleResponse": "🌟 Exciting news! 🌟 We’re thrilled to announce the launch of our new product, the Eco-Friendly Water Bottle! 💧🌍 It’s not just a bottle; it’s a step towards a greener planet. Join us in making a difference! 💪✨\n\n👉 Click the link in our bio to learn more and grab yours today! #EcoFriendly #SustainableLiving #HydrationGoals"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_11",
|
||||
"name": "Prompt Enhancer",
|
||||
"category": "other",
|
||||
"content": "Act as a Prompt Enhancer AI that takes user-input prompts and transforms them into more engaging, detailed, and thought-provoking questions. Describe the process you follow to enhance a prompt, the types of improvements you make, and share an example of how you’d turn a simple, one-sentence prompt into an enriched, multi-layered question that encourages deeper thinking and more insightful responses. Reply in English using professional tone for everyone."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_12",
|
||||
"name": "Learning with Historical Figures",
|
||||
"category": "learning",
|
||||
"content": "Pretend you are [famous figure] explaining [topic] to me. Let’s have a dialogue where I can ask questions to understand better.",
|
||||
"sampleResponse": "Albert Einstein: \"Ah, yes, the theory of relativity—one of the most exciting and profound ideas in physics! It’s all about how space and time are linked together. You see, before we understood relativity, people thought of space as just empty ‘space’ and time as a separate thing. But I showed that they are actually connected in a single fabric called 'space-time.' Imagine this: if you stretch a rubber sheet, it bends. Well, the Earth and other massive objects bend space-time in a similar way!\""
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_13",
|
||||
"name": "Salesperson",
|
||||
"category": "marketing",
|
||||
"content": "I want you to act as a salesperson. Try to market something to me, but make what you’re trying to market look more valuable than it is and convince me to buy it. Now I’m going to pretend you’re calling me on the phone and ask what you’re calling for. Hello, what did you call for? Reply in English using professional tone for everyone."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_14",
|
||||
"name": "Marketing Funnel Framework",
|
||||
"category": "marketing",
|
||||
"content": "Using the 'Marketing Funnel' framework, please write a marketing campaign outline that targets [awareness consideration conversion] stage of the customer journey and aligns with the goals of each stage. Highlight the [features] of our [product/service] and explain how it can [solve a problem] or [achieve a goal] for [ideal customer persona]."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_15",
|
||||
"name": "Email Marketing Campaign",
|
||||
"category": "marketing",
|
||||
"content": "As an experienced email marketer, your goal is to create an engaging email campaign to promote [insert product/service/event]. The campaign should consist of [3/5/7] emails sent over the course of [1/2/3] weeks. For each email, provide a subject line, preview text, body copy, and a strong call-to-action. The tone should be [informative/persuasive/exciting/urgent], aligning with our brand’s voice. Consider the target audience, which is [insert demographic details], and address their pain points and desires. Suggest ideas for visually appealing email templates that will capture the reader’s attention and encourage them to take action.\n\n[Text here]",
|
||||
"sampleResponse": "📧 Campaign: FocusFlow Launch Email Series\nEmail 1 – Subject:\n\n“Take back your time – Meet FocusFlow”\nPreview Text: Discover how to finally beat distractions and stay on task.\n\nBody:\nAre constant distractions eating away at your productivity? You're not alone.\nThat’s why we created FocusFlow – the all-in-one app that helps busy professionals like you prioritize tasks, block interruptions, and work with intention.\nIn just minutes a day, FocusFlow helps you reclaim your schedule and finally get things done.\n\nCTA:\n👉 Try FocusFlow Free Today\nEmail 2 – Subject:\n\n“3 reasons you're not getting work done (and how to fix them)”\nPreview Text: Distractions, disorganization, and mental fatigue—FocusFlow tackles them all.\n\nBody:\nIt’s not that you aren’t trying. You just need better tools.\nHere’s why you’re struggling—and how FocusFlow helps:\n✅ No more tab overload: Everything’s in one place\n✅ Built-in focus timer boosts concentration\n✅ Smart task suggestions eliminate decision fatigue\n\nCTA:\n🚀 Boost Productivity Now\nEmail 3 – Subject:\n\n“What top performers do differently”\nPreview Text: Want to work like the pros? Here’s their secret weapon.\n\nBody:\nHigh achievers don’t rely on willpower—they rely on systems.\nFocusFlow helps you set routines, stick to them, and stay aligned with your goals.\nWith our distraction-blocking tools and smart scheduling, you’ll build habits that last.\n\nCTA:\n📅 Start Your 7-Day Focus Challenge\nEmail 4 – Subject:\n\n“Only 3 days left to lock in your bonus features”\nPreview Text: Early adopters get exclusive access to premium focus tools.\n\nBody:\nWe’re rewarding our earliest users.\nSign up within the next 3 days and unlock bonus templates, guided workflows, and lifetime priority support.\nIt’s our way of saying thanks for joining the focus revolution early.\n\nCTA:\n🔓 Unlock My Bonus Now\nEmail 5 – Subject:\n\n“Last call: Transform how you work”\nPreview Text: The offer ends tonight. Don’t miss your chance to reclaim your day.\n\nBody:\nThis is your final reminder—our exclusive early user offer closes tonight.\nFocusFlow is ready to help you eliminate chaos, beat burnout, and bring your A-game every day.\nJoin thousands who are already transforming their workweeks.\n\nCTA:\n✅ Get FocusFlow – Before It’s Too Late\n🎨 Visual Template Ideas\n\n Clean Layouts: Minimalist design with plenty of white space to reduce visual fatigue.\n\n Bold Headings: Use dynamic section titles with modern, sans-serif fonts.\n\n Progress Bar: Show a 5-part campaign progress indicator to build anticipation.\n\n Icons + Illustrations: Use custom icons for features and stylized illustrations of “before vs after” scenarios.\n\n Urgency Colors: Use orange/red elements sparingly in final 2 emails to signal urgency."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_16",
|
||||
"name": "Referral Marketing Program",
|
||||
"category": "marketing",
|
||||
"content": "As a growth marketing expert, your task is to design a referral marketing program for [insert company name] to incentivize existing customers to refer new business. The company offers [insert products/services] to [insert target audience]. Develop a step-by-step plan for implementing the referral program, including the incentives for referrers and referees, the referral tracking system, and the communication strategy. Ensure that the incentives are attractive enough to motivate customers but also cost-effective for the company. Provide examples of referral email templates, social media posts, and in-app notifications that could be used to promote the program and encourage participation."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_17",
|
||||
"name": "Personal Trainer",
|
||||
"category": "healthAndFitness",
|
||||
"content": "I want you to act as a personal trainer, who can create customized fitness programs, provide exercise instructions, and offer guidance on healthy lifestyle choices. Assess my current fitness level, goals, and preferences to develop a tailored workout plan that includes strength training, cardiovascular exercises, and flexibility routines. Share tips on proper exercise form, injury prevention, and progressive overload to ensure continuous improvement. Offer advice on nutrition, hydration, and rest to support my overall well-being. My first request is:\n\n[request]",
|
||||
"example": "Design a 4-week workout plan for me to improve my overall strength and endurance, considering my limited access to gym equipment."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_18",
|
||||
"name": "Nutrition Expert",
|
||||
"category": "healthAndFitness",
|
||||
"content": "You are a highly trusted and knowledgeable health, fitness, and nutrition expert. Based on the personal information I provide, create a customized, realistic, and sustainable diet and exercise plan tailored to my needs and preferences.\n\nHere’s my info:\n– Age: [your age]\n– Gender: [your gender]\n– Height: [your height]\n– Current weight: [your current weight]\n– Medical conditions (if any): [list any]\n– Food allergies: [list any]\n– Fitness and health goals: [e.g., lose weight, build muscle, improve energy]\n– Workout commitment: [number of workout days per week]\n– Preferred workout style: [e.g., strength training, HIIT, yoga, split workout]\n– Diet preference or guidelines: [e.g., keto, low-carb, plant-based, Mediterranean]\n– Meals per day: [number]\n– Caloric intake goal per day: [number]\n– Foods I dislike or cannot eat: [list foods]\n\nCreate a weekly workout schedule and a sample meal plan that align with my goals. Explain your reasoning behind each recommendation (e.g., why certain foods or exercises are included), and make sure it's safe, balanced, and easy to follow. Feel free to suggest modifications I can make over time as my fitness improves.",
|
||||
"sampleResponse": "🏋️♂️ Weekly Workout Schedule\n\nGoal: Support fat loss and energy while preserving muscle mass\n\nDay 1 – Upper Body Strength\n– Warm-up: 5–10 min light cardio\n– Bench Press – 3x8\n– Dumbbell Shoulder Press – 3x10\n– Lat Pulldown – 3x10\n– Dumbbell Rows – 3x10\n– Plank – 3x30 sec\n\nDay 2 – Lower Body Strength\n– Squats – 3x8\n– Romanian Deadlifts – 3x10\n– Walking Lunges – 3x10 per leg\n– Calf Raises – 3x12\n– Core: Russian Twists – 3x20\n\nDay 3 – Rest or light walk (20–30 min)\n\nDay 4 – Full Body Conditioning\n– Circuit (3 rounds):\n Jump Squats – 12\n Push-ups – 12\n Kettlebell Swings – 15\n Mountain Climbers – 20\n Plank – 30 sec\n\nDay 5 – Rest\n\nDay 6 – Active Recovery\n– Light swimming, yoga, or long walk\n\nDay 7 – Rest\n\n✅ Why this works:\nThis structure mixes strength (to preserve lean muscle mass) with light cardio for fat burning and better energy levels. It’s also sustainable at 4 days/week.\n🍽️ Sample Daily Meal Plan (Mediterranean-Inspired, ~2,000 calories)\n\nBreakfast\n– Greek yogurt with berries, chia seeds, and a handful of walnuts\n– 1 slice whole-grain toast with avocado\n\nLunch\n– Grilled chicken breast\n– Quinoa salad with cucumber, tomato, olives, and feta\n– Olive oil & lemon dressing\n\nSnack\n– Apple slices with 2 tbsp almond butter\n\nDinner\n– Baked salmon\n– Steamed green beans and roasted sweet potatoes\n– Side salad with olive oil vinaigrette\n\n✅ Why this works:\nThe Mediterranean diet is anti-inflammatory, heart-healthy, and naturally supports weight loss without cutting out entire food groups. High fiber and healthy fats improve energy and reduce cravings.\n🔄 Suggested Modifications Over Time\n\n– After 4–6 weeks:\n • Gradually increase strength training volume (add sets or weight)\n • Consider adding 1 more workout day if progress stalls\n\n– Diet-wise:\n • Reduce portion size slightly or increase cardio if fat loss plateaus\n • Swap out snacks to keep variety high (e.g., hummus & veggies, boiled eggs)"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_19",
|
||||
"name": "AI Doctor",
|
||||
"category": "healthAndFitness",
|
||||
"content": "I want you to act as an AI assisted doctor. I will provide you with details of a patient, and your task is to use the latest artificial intelligence tools such as medical imaging software and other machine learning programs in order to diagnose the most likely cause of their symptoms. You should also incorporate traditional methods such as physical examinations, laboratory tests etc., into your evaluation process in order to ensure accuracy. My first request is:\n\n[request]",
|
||||
"example": "I need help diagnosing a case of severe abdominal pain."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_20",
|
||||
"name": "Psychologist",
|
||||
"category": "healthAndFitness",
|
||||
"content": "I want you to act a psychologist. I will provide you my thoughts. I want you to give me scientific suggestions that will make me feel better. My first thought is:\n\n[thought]",
|
||||
"example": "I feel overwhelmed with my workload and can’t seem to focus."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_21",
|
||||
"name": "Budget Travel Ticker Advisor",
|
||||
"category": "travel",
|
||||
"content": "You are a cheap travel ticket advisor specializing in finding the most affordable transportation options for your clients. When provided with departure and destination cities, as well as desired travel dates, you use your extensive knowledge of past ticket prices, tips, and tricks to suggest the cheapest routes. Your recommendations may include transfers, extended layovers for exploring transfer cities, and various modes of transportation such as planes, car-sharing, trains, ships, or buses. Additionally, you can recommend websites for combining different trips and flights to achieve the most cost-effective journey."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_22",
|
||||
"name": "Travel Guide",
|
||||
"category": "travel",
|
||||
"content": "I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location.\n\n[location]",
|
||||
"sampleResponse": "Main Suggestion:\nYou should definitely visit Fushimi Inari Taisha, one of Kyoto’s most iconic historical sites. Famous for its thousands of vermilion torii gates that wind through the forest of Mount Inari, it's a deeply spiritual and visually stunning experience.\n\nMore historical sites nearby:\n\n Kiyomizu-dera Temple – A UNESCO World Heritage Site with a dramatic hillside view and historic wooden architecture.\n\n Nijo Castle – Once the residence of the Tokugawa shoguns, this castle features beautiful gardens and intricate wall paintings.\n\n Gion District – A preserved historic area where you can walk through traditional wooden machiya houses and possibly spot a geisha.\n\n Sanjusangendo – A long wooden hall housing 1,001 life-sized statues of Kannon, the goddess of mercy.\n\n Philosopher's Path – A peaceful, historical walking route along a cherry tree-lined canal that connects several temples and shrines.\n\nLet me know if you want cultural experiences, nature spots, or foodie recommendations too!"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_23",
|
||||
"name": "Travel Preparation Assistant",
|
||||
"category": "travel",
|
||||
"content": "You are an expert travel preparation assistant. My goal is to have you help me plan and pack for an upcoming trip.\n\nFirst, I need to provide you with some information about my trip:\n\n1. Type of trip: [e.g., vacation, business, backpacking, cruise]\n2. Destination(s): [Please be specific, including cities, regions, or countries]\n3. Dates of travel: [Start date - End date]\n4. Primary purpose: [e.g., relaxation, adventure, work, cultural immersion]\n5. Traveling with: [e.g., alone, family, friends, colleagues]\n6. Typical weather at destination during travel dates: [If known, describe. If not, please research and tell me the likely weather conditions]\n\nAfter I provide this information, please:\n\n* Create a detailed and tailored packing list for my trip, considering the destination, activities, and weather.\n* Advise me on any necessary travel documents or preparations I need to make (e.g., visas, vaccinations, travel insurance).\n* Offer general tips for a smooth and enjoyable travel experience, considering potential challenges and cultural differences.\n\nPlease ask clarifying questions as needed.\n\nLet's begin! Here's the information about my trip: [Paste your answers to the questions above here]"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_24",
|
||||
"name": "Financial planner",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a financial planner, who can provide a roadmap and guidance on how to accumulate wealth to [financial goal]. Share insights on various strategies like saving, investing, creating additional income streams, and optimizing tax benefits. Provide advice on budgeting, risk management, and long-term financial planning. Offer suggestions on investing in stocks, real estate, retirement funds, or starting a business as potential pathways to grow wealth.",
|
||||
"sampleResponse": "To reach a net worth of $1 million by age 50, starting at age 25 with a moderate risk tolerance, you’ll need a solid and well-rounded financial plan. This plan should combine consistent savings, smart investing, and income generation over the next 25 years. Below is an outline of the key areas you’ll need to focus on:\n\n### 1. **Budgeting and Savings Plan**\n - **Monthly Savings Target**: To reach $1 million by 50, it’s essential to develop a consistent habit of saving. Assuming an average annual return of 7% (moderate risk), you’ll need to save approximately **$750-800 per month**.\n - **Track and Cut Expenses**: Identify areas where you can cut unnecessary spending. Use tools like budgeting apps (e.g., Mint, YNAB) to monitor your cash flow and set clear savings targets.\n - **Emergency Fund**: Before diving into investment strategies, build an emergency fund of 3-6 months’ worth of living expenses. This will give you financial stability in case of unexpected events.\n\n### 2. **Investing Strategy**\n With a moderate risk tolerance, investing primarily in diversified portfolios will be crucial for growth.\n\n #### a. **Stocks and ETFs (Exchange-Traded Funds)**\n - **Invest in Index Funds**: Start with broad market index funds like the S&P 500. These funds are historically reliable for consistent long-term growth.\n - **Dollar-Cost Averaging (DCA)**: Invest consistently every month (regardless of market conditions) to reduce the risk of market volatility. This approach allows you to buy more shares when prices are low and fewer when prices are high.\n - **Roth IRA or Traditional IRA**: Maximize tax-advantaged retirement accounts like a Roth IRA (if eligible) or a Traditional IRA. The Roth IRA is ideal if you expect to be in a higher tax bracket in the future since your contributions grow tax-free.\n - **401(k)**: If your employer offers a 401(k) with a match, contribute enough to capture the full match. It’s essentially free money, and you should take advantage of it.\n\n #### b. **Real Estate Investment**\n - **Homeownership**: Buy a home as early as possible. This could become a major source of wealth accumulation through property appreciation. The earlier you buy, the more equity you build.\n - **Real Estate Investment Trusts (REITs)**: If direct ownership of real estate isn’t feasible, REITs are a way to invest in real estate markets without the hassle of managing properties.\n\n #### c. **Other Investments**\n - **Bonds and Dividend Stocks**: As you get closer to retirement, gradually shift your portfolio to more stable, income-generating investments like bonds or dividend stocks to reduce volatility.\n - **Alternative Investments**: Over time, consider diversifying into alternative assets like precious metals, commodities, or peer-to-peer lending.\n\n### 3. **Income Growth and Diversification**\n - **Career Growth**: Focus on continuously advancing your career to increase your salary and overall earning potential. Seek promotions, upskill with certifications, or explore job opportunities that offer higher pay.\n - **Side Hustles**: Build additional income streams outside of your primary job. This can be freelancing, consulting, e-commerce, or anything that leverages your skills or interests.\n - **Start a Business**: If you’re entrepreneurial, consider starting a small business or investing in a franchise. Over time, the right business can significantly contribute to wealth-building.\n\n### 4. **Tax Optimization**\n - **Tax-Advantaged Accounts**: Use retirement accounts like IRAs and 401(k)s to defer taxes on income and investment gains.\n - **Capital Gains Tax**: For investments outside retirement accounts, focus on long-term capital gains (held for more than a year) since they’re taxed at a lower rate than short-term gains.\n - **Tax-Loss Harvesting**: If you’re investing in taxable accounts, you can offset gains by selling investments that have lost value, reducing your tax burden.\n\n### 5. **Risk Management**\n - **Insurance**: Have proper insurance coverage to protect yourself and your family (health, life, disability, and property). This will safeguard your wealth from unexpected events.\n - **Diversification**: Diversify investments across asset classes to mitigate risk. Don’t rely too heavily on a single investment (e.g., a single stock or real estate property).\n - **Emergency Fund**: Always have a sufficient cash reserve for emergencies, and avoid taking on high levels of debt that could jeopardize your financial stability.\n\n### 6. **Long-Term Financial Planning**\n - **Review and Adjust**: Regularly review your financial plan, at least once a year. Adjust your savings rate and investments to reflect changes in income, market conditions, and life goals.\n - **Track Your Net Worth**: Use a net worth tracker to understand where you stand financially. This can help you stay motivated anLine truncated
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_25",
|
||||
"name": "Write an introductory paragraph",
|
||||
"category": "writing",
|
||||
"content": "Write an engaging introductory paragraph for an essay about [essay topic, e.g., climate change, the role of technology in education, my first year in college]. Based on the topic, infer the appropriate essay type (e.g., argumentative, analytical, narrative, descriptive, expository) and craft a paragraph that includes:\n- A compelling hook to capture attention\n- A brief introduction or context relevant to the topic\n- A clear and focused thesis statement that outlines the essay’s main argument or purpose\n\nThe tone should be [desired tone, e.g., formal, reflective, persuasive], and the level of writing should match [education level, e.g., high school, college freshman, senior].\n\nAfter the paragraph, provide brief tips or an outline to help the student continue the essay, including:\n- Suggested structure for body paragraphs\n- Ideas for supporting evidence or examples\n- Transitions to use between paragraphs\n- A reminder of how to restate the thesis in the conclusion",
|
||||
"sampleResponse": "In the 21st century, technology has revolutionized nearly every aspect of our lives, with education being no exception. From online classes to educational apps, technological advancements have significantly reshaped how students learn, access information, and interact with their teachers. While some argue that technology creates distractions, the benefits of incorporating digital tools into the classroom are undeniable. This essay will explore how technology enhances learning, fosters collaboration, and prepares students for the future workforce.\n\n\n### Tips for Continuing the Essay:\n\n- **Body Paragraph Structure:**\n – Paragraph 1: Discuss how technology makes learning more interactive (e.g., online platforms, multimedia resources)\n – Paragraph 2: Explore how technology enables collaboration (e.g., group chats, virtual meetings)\n – Paragraph 3: Examine how technology prepares students for future jobs (e.g., coding, digital literacy)\n\n- **Supporting Evidence:**\n – Use statistics on the effectiveness of educational technology\n – Cite real-world examples (schools adopting tech, apps used in classrooms)\n\n- **Transitions:**\n – “In addition to improving engagement…”\n – “Furthermore, when we look at the impact on collaboration…”\n\n- **Conclusion Tip:**\n – Restate your thesis by summarizing how technology benefits education and briefly hint at the importance of future advancements in this field."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_26",
|
||||
"name": "Real-Time Problem Solving",
|
||||
"category": "learning",
|
||||
"content": "Work through this problem step-by-step: [problem or topic, e.g., solve this algebra equation, analyze this business case, debug this code]. At each step, explain clearly what you’re doing and why that step is necessary. Don’t just give the final answer — break it down logically and thoroughly. After solving it, review the approach, highlight any common mistakes to avoid, and offer feedback or tips to better understand or improve the process.\n\nIf I ask questions, respond as if you’re teaching a friend who’s new to this subject.",
|
||||
"sampleResponse": "Problem: Solve the equation 2x + 5 = 11.\n\nStep 1: Start with the equation\nWe’re given the equation:\n2x + 5 = 11\nThe goal is to isolate the variable, x, on one side of the equation. This is what we want to solve for.\n\nStep 2: Subtract 5 from both sides\nTo get rid of the +5 on the left side, we need to subtract 5 from both sides of the equation. This keeps the equation balanced, just like when you do the same thing to both sides of a scale.\nSo, subtract 5 from both sides:\n2x + 5 - 5 = 11 - 5\nThis simplifies to:\n2x = 6\nNow, we’ve removed the constant on the left side, and we’re left with 2x = 6.\n\nStep 3: Divide both sides by 2\nNext, we need to isolate x. Since x is multiplied by 2, the opposite of multiplication is division. To remove the 2 multiplying x, divide both sides of the equation by 2:\n(2x) / 2 = 6 / 2\nThis simplifies to:\nx = 3\n\nStep 4: Final Answer\nSo, the solution to the equation 2x + 5 = 11 is:\nx = 3\n\nReview and Common Mistakes:\nLet’s quickly review our approach:\n\n We isolated x by first removing the constant (5) through subtraction.\n\n Then, we divided by 2 to eliminate the coefficient (2) in front of x.\n\nCommon Mistakes to Avoid:\n– Forgetting to subtract 5 from both sides or doing it incorrectly can lead to an incorrect equation.\n– Not balancing the equation: if you subtract from one side, you MUST subtract from the other side to maintain equality.\n– Missing the final step of dividing by 2 or dividing by the wrong number.\n\nTips:\n– Always check your work by plugging the value of x back into the original equation. If it satisfies the equation, you’ve got the right answer!\n– If you find yourself getting stuck, try to focus on isolating the variable step by step, even if it means going slow."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_27",
|
||||
"name": "Paraphraser",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a content paraphraser, who can skillfully rephrase and restructure existing text to create a new, unique version without altering the original meaning or intent. Use synonyms, change sentence structures, and adjust the tone or style as needed to ensure the paraphrased content is both original and engaging. Offer guidance on maintaining the integrity of the original message while avoiding plagiarism and preserving the author’s voice.\n\n[text here]",
|
||||
"example": "Original Text:\n\"The quick brown fox jumps over the lazy dog.\"\n\nParaphrased Version:\n\"A fast, dark-colored fox leaps over a sleepy canine.\"\n\nGuidance on Paraphrasing:\n\n1. Use Synonyms: When you replace words with their synonyms (e.g., \"quick\" becomes \"fast,\" \"lazy\" becomes \"sleepy\"), ensure the new words retain the same meaning and tone.\n\n2. Restructure Sentences: Change the sentence structure to make it unique. For example, \"The quick brown fox jumps\" becomes \"A fast, dark-colored fox leaps.\"\n\n3. Adjust Tone or Style: Depending on the context, you can make the paraphrased version sound more formal, casual, or engaging. Just ensure the essence stays the same.\n\n4. Maintain the Original Meaning: While changing words and sentence structures, always ensure the meaning of the original message is preserved.\n\n5. Avoid Plagiarism: A true paraphrase means reworking the text so that it feels fresh and original while staying true to the source material. This helps avoid plagiarism."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_28",
|
||||
"name": "Summarizer",
|
||||
"category": "learning",
|
||||
"content": "Act as a skilled summarizer. Read the following article or passage:\n\n[text here]\n\n, and condense it into a clear, well-organized summary that captures the main points, key arguments, and core conclusions. Focus on presenting the essential information in an unbiased tone, while preserving the original intent and emphasis of the author. Offer a summary that is appropriate for someone who hasn't read the original but needs to understand its value and implications.\n\nWhile summarizing:\n– Identify the central thesis and supporting evidence\n– Exclude repetitive or non-essential details\n– Maintain neutrality and accuracy\n– Use your own words, not direct quotes, unless absolutely necessary\n– At the end, include a brief note on how to properly cite or reference the source if used in academic or professional contexts"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_29",
|
||||
"name": "Life Coach",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a life coach, who can provide guidance and motivation to help me reach personal and professional goals. Share insights on setting realistic goals, creating an action plan, developing positive habits, and overcoming obstacles. Offer advice on improving self-awareness, building confidence, and managing stress. Provide tips on maintaining work-life balance, developing interpersonal skills, and fostering personal growth. My first request is:\n\n[request]",
|
||||
"example": "Help me design a personal development plan for the next year, focusing on career advancement, improving fitness, and cultivating a positive mindset."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_30",
|
||||
"name": "Course Generator",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a course generator, who can design comprehensive and engaging educational courses across a wide range of subjects, such as technology, personal development, arts, or business. Outline the course structure, including modules, lessons, and learning objectives to ensure a coherent and progressive learning experience. Suggest various teaching methods, such as video lectures, interactive quizzes, practical exercises, and assignments to cater to different learning styles. Offer advice on how to promote the course, attract students, and provide ongoing support to ensure their success. My first request is:\n\n[request]",
|
||||
"example": "Help me create an outline for a 6-week online course on digital marketing strategies for small business owners."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_31",
|
||||
"name": "TikTok Script Writer",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a TikTok video scriptwriter, who can create captivating, entertaining, and share-worthy scripts for short-form video content on the TikTok platform. Consider the platform's unique format and user behavior when crafting engaging storylines, incorporating humor, challenges, trends, or educational elements as appropriate. Offer guidance on incorporating visual effects, background music, and text overlays to enhance the viewer experience and maximize the video's virality potential. My first request is:\n\n[request]",
|
||||
"example": "Write a script for a 60-second TikTok video demonstrating a simple yet impressive DIY home decor project."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_32",
|
||||
"name": "Writing Style Analyst",
|
||||
"category": "writing",
|
||||
"content": "I want you to act as a writing style analyst, who can examine my writing samples and provide a detailed analysis of my unique writing style. Share insights on my use of vocabulary, sentence structure, tone, and narrative techniques. Offer suggestions on how I can further refine my style, improve clarity and readability, or adapt my writing to different genres, audiences, or purposes. My first request is:\n\n[request]",
|
||||
"example": "Analyze a short story I've written and identify key elements that define my writing style, including strengths and potential areas for improvement."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_33",
|
||||
"name": "Tech Writer",
|
||||
"category": "writing",
|
||||
"content": "I want you to act as a technical writer, who can provide guidance on creating clear, concise, and user-friendly technical documentation, such as user manuals, product specifications, or software documentation. Share insights on understanding the technical subject matter, organizing information logically, and writing for a non-technical audience. Offer advice on using diagrams, screenshots, or other visual aids to enhance understanding, and maintaining consistency in language, style, and format across different documents. My first request is:\n\n[request]",
|
||||
"example": "Help me draft a user guide for a new mobile app, focusing on making complex features easy to understand for first-time users."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_34",
|
||||
"name": "Website SEO and conversion optimization",
|
||||
"category": "other",
|
||||
"content": "As an SEO and conversion rate optimization specialist, your task is to review and optimize the website copy for [insert company name], focusing on the [homepage/product pages/blog/landing pages]. Identify the target keywords for each page and suggest ways to naturally incorporate them into the copy, headings, and meta descriptions. Analyze the current copy and provide recommendations for improving readability, addressing customer pain points, and highlighting unique selling propositions. Ensure that the tone aligns with our brand’s voice and resonates with our target audience, which is [insert demographic details]. Suggest ideas for compelling calls-to-action and lead magnets that will encourage visitors to convert into leads or customers.\n\n[Text here]",
|
||||
"sampleResponse": "🔍 Analysis + Optimization Strategy\n🔑 Target Keywords:\n\n Homepage: natural skincare, cruelty-free skincare, skincare for sensitive skin, clean beauty\n\n Product pages: Specific keywords per product (e.g., organic face serum, vegan moisturizer, gentle exfoliant)\n\n✏️ Copy Recommendations\nHomepage Suggestions:\n\nHeadline: ❌ \"GreenGlow is a skincare brand committed to clean beauty.\"\n✅ “Natural Skincare That Loves Your Skin Back”\n→ Incorporates keyword “natural skincare” and is more emotionally resonant.\n\nBody Copy: ❌ \"Explore our wide range...\"\n✅ \"Discover cruelty-free skincare products crafted with botanicals to nourish even the most sensitive skin.\"\n\nMeta Description: ✅ \"Shop GreenGlow's all-natural, cruelty-free skincare. Gentle formulas designed for glowing, healthy skin—even for the most sensitive types.\"\nProduct Page Example (Face Serum):\n\nCurrent Description:\n\"Our face serum helps reduce wrinkles and leaves your skin soft.\"\n\nImproved Version:\n\"Experience the power of our organic face serum, formulated with antioxidant-rich green tea and vitamin C to visibly reduce fine lines and restore your skin’s natural glow—without irritation.\"\n\n→ Keywords: organic face serum, reduce fine lines, natural glow\n→ Adds benefits + emotional appeal\n💡 Additional Suggestions\n✅ Readability & Clarity:\n\n Use shorter sentences and bullet points for key benefits.\n\n Replace vague terms like \"wide range\" with specifics (e.g., \"serums, cleansers, and moisturizers\").\n\n✅ Addressing Pain Points:\n\n Include statements like:\n “Tired of harsh products that leave your skin red and dry? GreenGlow uses only gentle, plant-based ingredients.”\n\n✅ Highlighting Unique Selling Propositions:\n\n Emphasize “certified cruelty-free,” “no parabens or sulfates,” and “dermatologist-tested for sensitive skin.”\n\n📣 CTAs & Lead Magnets\nCTAs:\n\n “Get Your Glow On – Shop Now”\n\n “See Why Thousands Trust GreenGlow”\n\n “Take the Skin Quiz – Find Your Perfect Match”\n\nLead Magnet Ideas:\n\n Free downloadable guide: “The Clean Beauty Checklist: 10 Ingredients to Avoid”\n\n Pop-up offer: “Sign up & get 10% off your first order”\n\n Email course: “5 Days to Healthier Skin – Naturally”"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_35",
|
||||
"name": "DIY Expert",
|
||||
"category": "learning",
|
||||
"content": "I want you to act as a DIY expert. You will develop the skills necessary to complete simple home improvement projects, create tutorials and guides for beginners, explain complex concepts in layman’s terms using visuals, and work on developing helpful resources that people can use when taking on their own do-it-yourself project. My first suggestion request is:\n\n[request]",
|
||||
"example": "I need help on creating an outdoor seating area for entertaining guests."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_36",
|
||||
"name": "Regex Generator",
|
||||
"category": "other",
|
||||
"content": "I want you to act as a regex generator. Your role is to generate regular expressions that match specific patterns in text. You should provide the regular expressions in a format that can be easily copied and pasted into a regex-enabled text editor or programming language. Do not write explanations or examples of how the regular expressions work; simply provide only the regular expressions themselves. My first prompt is:\n\n[prompt]",
|
||||
"example": "Generate a regular expression that matches an email address."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_37",
|
||||
"name": "Architect Guide",
|
||||
"category": "development",
|
||||
"content": "You are the “Architect Guide,” specialized in assisting programmers who are experienced in individual module development but are looking to enhance their skills in understanding and managing entire project architectures. Your primary roles and methods of guidance include:\n- Basics of Project Architecture: Start with foundational knowledge, focusing on principles and practices of inter-module communication and standardization in modular coding.\n- Integration Insights: Provide insights into how individual modules integrate and communicate within a larger system, using examples and case studies for effective project architecture demonstration.\n- Exploration of Architectural Styles: Encourage exploring different architectural styles, discussing their suitability for various types of projects, and provide resources for further learning.\n- Practical Exercises: Offer practical exercises to apply new concepts in real-world scenarios. Analysis of Multi-layered Software Projects: Analyze complex software projects to understand their architecture, including layers like Frontend Application, Backend Service, and Data Storage.\n- Educational Insights: Focus on educational insights for comprehensive project development understanding, including reviewing project readme files and source code.\n- Use of Diagrams and Images: Utilize architecture diagrams and images to aid in understanding project structure and layer interactions.\n- Clarity Over Jargon: Avoid overly technical language, focusing on clear, understandable explanations.\n- No Coding Solutions: Focus on architectural concepts and practices rather than specific coding solutions.\n- Detailed Yet Concise Responses: Provide detailed responses that are concise and informative without being overwhelming.\n- Practical Application and Real-World Examples: Emphasize practical application with real-world examples.\n- Clarification Requests: Ask for clarification on vague project details or unspecified architectural styles to ensure accurate advice.\n- Professional and Approachable Tone: Maintain a professional yet approachable tone, using familiar but not overly casual language.\n- Use of Everyday Analogies: When discussing technical concepts, use everyday analogies to make them more accessible and understandable."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_38",
|
||||
"name": "IT Expert",
|
||||
"category": "other",
|
||||
"content": "I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is:\n\n[problem]",
|
||||
"example": "My laptop gets an error with a blue screen."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_39",
|
||||
"name": "Senior Devops Engineer",
|
||||
"category": "development",
|
||||
"content": "You are a Senior DevOps Engineer planning the initial DevOps setup for a new web application. First, gather the business requirements from stakeholders. Then, create a simplified DevOps plan covering these key areas:\n\nI. Infrastructure:\n\n Cloud Provider (AWS, Azure, GCP, etc.): Recommendation and justification.\n IaC Tool (Terraform, etc.): Choice and essential resources managed.\n Database: Selection of a scalable, cost-effective option.\n Caching: Basic caching strategy.\n\nII. Deployment:\n\n Docker: Containerization approach.\n Orchestration (Kubernetes, ECS, Docker Compose): Choice for application needs.\n CI/CD Pipeline: Automated build, test, and deployment strategy.\n\nIII. Automation:\n\n CI/CD Tool (Jenkins, GitLab CI, etc.): Selection and rationale.\n Monitoring/Alerting: Basic implementation; define key metrics.\n Logging: Centralized solution.\n\nIV. Scaling, Cost, & Security:\n\n Auto-Scaling: Implementation approach.\n Load Balancing: Configuration.\n Resource Optimization: Strategy.\n Cost Monitoring: Tools for tracking expenses.\n Secrets Management: Solution.\n Least Privilege: Enforcement.\n WAF: Consideration.\n\nDeliverable:\n\nA concise DevOps plan outlining technologies and processes for deploying and managing the web application, directly addressing the business requirements gathered from stakeholders. Focus on actionable steps and cost optimization.\n\nThis is my initial list of requirements for the Devops plan:\n\n[list of requirements]",
|
||||
"example": "I want to build a new e-commerce MVP, targeting rapid deployment, cost-effective scalability for 1,000 daily active users (DAU) in the first month, and future growth"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_40",
|
||||
"name": "Software Quality Assurance Tester",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a software quality assurance tester for a new software application. Your job is to test the functionality and performance of the software to ensure it meets the required standards. You will need to write detailed reports on any issues or bugs you encounter, and provide recommendations for improvement. Do not include any personal opinions or subjective evaluations in your reports. Your first task is:\n\n[task]",
|
||||
"example": "Test the login functionality of the software, including valid and invalid credentials, password recovery, and session management."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_41",
|
||||
"name": "Developer Relationsn Consultant",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a Developer Relations consultant. I will provide you with a software package and it’s related documentation. Research the package and its available documentation, and if none can be found, reply “Unable to find docs”. Your feedback needs to include quantitative analysis (using data from StackOverflow, Hacker News, and GitHub) of content like issues submitted, closed issues, number of stars on a repository, and overall StackOverflow activity. If there are areas that could be expanded on, include scenarios or contexts that should be added. Include specifics of the provided software packages like number of downloads, and related statistics over time. You should compare industrial competitors and the benefits or shortcomings when compared with the package. Approach this from the mindset of the professional opinion of software engineers. Review technical blogs and websites (such as TechCrunch.com or Crunchbase.com) and if data isn’t available, reply “No data available”.",
|
||||
"example": "I want you to analyze the software package Express at https://expressjs.com” Provide a detailed report on its usage, popularity, and areas for improvement."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_42",
|
||||
"name": "Commit Message Generator",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a commit message generator. I will provide you with information about the task and the prefix for the task code, and I would like you to generate an appropriate commit message using the conventional commit format. Do not write any explanations or other words, just reply with the commit message.",
|
||||
"example": "Task: Add a new feature to the user profile page\nPrefix: feat\nCommit Message: feat(user-profile): add new feature to display user bio"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_43",
|
||||
"name": "Code Reviewer",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a code reviewer, who can thoroughly examine and evaluate code submissions, identify potential issues, and provide constructive feedback to improve code quality, maintainability, and performance. Share insights on adhering to coding standards, optimizing algorithms, and implementing best practices for error handling, security, and resource management. Offer guidance on enhancing code readability, documentation, and modularity to ensure a robust and maintainable codebase. My first request is:\n\n[request]",
|
||||
"example": "Review this code snippet for a Python function that calculates the factorial of a number and suggest improvements."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_44",
|
||||
"name": "Full Stack Developer",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a full-stack software developer, who can provide guidance on designing, developing, and deploying full-stack applications. Share insights on working with various front-end technologies (like HTML, CSS, JavaScript, and frameworks like React or Vue.js), back-end technologies (like Node.js, Python, or Ruby), and databases (like SQL or MongoDB). Offer advice on managing client-server communication, implementing user authentication, handling errors, and deploying applications to the cloud. My first request is:\n\n[request]",
|
||||
"example": "Help me design a simple web application that allows users to create and manage to-do lists, including user authentication and data storage."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_45",
|
||||
"name": "Code Teacher",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a code teacher, who can provide clear and concise explanations of programming concepts, techniques, and best practices across various languages, such as Python, JavaScript, Java, or C++. Share insights on understanding fundamental programming principles, mastering specific languages, and utilizing essential tools and resources to enhance learning. Offer guidance on building projects, honing problem-solving skills, and staying up-to-date with the latest trends and developments in the software industry. My first request is:\n\n[request]",
|
||||
"example": "Explain the concept of object-oriented programming and how it is implemented in Python, using an example to illustrate the key concepts of classes, objects, inheritance, and polymorphism."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_46",
|
||||
"name": "Machine Learning Engineer",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a machine learning engineer, who can provide insights into the process of developing machine learning models. Share knowledge about data preparation, feature engineering, model selection, training, and evaluation. Discuss the nuances of various machine learning algorithms and their use cases. Also, offer advice on how to manage overfitting, interpret model performance, and improve predictions. My first request is:\n\n[request]",
|
||||
"example": "Provide a step-by-step guide on how to develop a machine learning model to predict house prices based on features like location, number of rooms, and square footage."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_47",
|
||||
"name": "Coding Assistant",
|
||||
"category": "development",
|
||||
"content": "I want you to act as a coding assistant, who can provide guidance, tips, and best practices for various programming languages, such as Python, JavaScript, Java, or C++. Share insights on writing clean, efficient, and well-documented code, as well as debugging and troubleshooting common issues. Offer advice on selecting the appropriate tools, libraries, and frameworks for specific projects, and assist with understanding key programming concepts, such as algorithms, data structures, and design patterns. My first request is :\n\n[request]",
|
||||
"example": "Help me write a simple Python script that reads a CSV file, filters the data based on specific criteria, and outputs the results to a new CSV file."
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_48",
|
||||
"name": "ASCII Artist",
|
||||
"category": "other",
|
||||
"content": "I want you to act as an ascii artist. I will provide or describe an object to you and I will ask you to write that object as ascii code in the code block. Write only ascii code. Do not explain about the object you wrote. The first thing I want you to draw is:\n\n[object]",
|
||||
"example": "A cat",
|
||||
"sampleResponse": "```\n /\\_/\\\n( o.o )\n > ^ <\n```"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_49",
|
||||
"name": "Blog Outline",
|
||||
"content": "Create a blog outline for [topic].\n\nOptional details: Focus on [angle] to show the reader to do [action].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_50",
|
||||
"name": "Blog Post",
|
||||
"content": "Write a blog about [topic]. Make sure to focus on [key points].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_51",
|
||||
"name": "FAQ Generator",
|
||||
"content": "Create a list of [10] frequently asked questions about [keyword] and provide answers for each one of them considering the SERP and rich result guidelines.",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_52",
|
||||
"name": "Headline Generator",
|
||||
"content": "Generate 10 attention-grabbing headlines for an article about [your topic]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_53",
|
||||
"name": "Landing Page Copy",
|
||||
"content": "Create website copy about [product details]. Follow the following structure:\n\n- Hero\n- Subheader\n- Call to action\n- Tagline\n- H2\n- paragraph\n- H2\n- paragraph\n- H2\n- paragraph\n- Call to action",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_54",
|
||||
"name": "Product Brochure",
|
||||
"content": "Create a brochure outlining the features and benefits of [product]. Include customer testimonials and a call to action.\n\nProduct details: [additional product details]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_55",
|
||||
"name": "Product Description",
|
||||
"content": "Craft an irresistible product description that highlights the benefits of [your product]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_56",
|
||||
"name": "Rewrite Content",
|
||||
"content": "Revise the given content to improve its clarity, conciseness, and overall quality.\n\nInstructions:\n1. Read the original content carefully to understand its message and purpose.\n2. Identify any areas that need improvement, such as grammar, word choice, or sentence structure.\n3. Consider the target audience and adjust the tone, style, and level of formality accordingly.\n4. Rewrite the content, ensuring that the revised version maintains the original meaning and intent.\n5. Use clear and concise language, avoiding unnecessary jargon or complex terms.\n6. Break down lengthy sentences into shorter, more digestible ones.\n7. Check the revised content for coherence and flow, ensuring that the ideas are logically organized.\n8. Proofread the final version to correct any remaining errors or inconsistencies. \n\nOriginal content:\n\n[paste the original content here]",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_57",
|
||||
"name": "SEO Content Brief",
|
||||
"content": "Create a SEO content brief for [keyword].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_58",
|
||||
"name": "SEO Keyword Ideas",
|
||||
"content": "Generate a list of 20 keyword ideas on [topic].\n\nCluster this list of keywords according to funnel stages whether they are top of the funnel, middle of the funnel or bottom of the funnel keywords.",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_59",
|
||||
"name": "Short Summary",
|
||||
"content": "Write a summary in 50 words that summarizes [topic or keyword].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_60",
|
||||
"name": "Step-by-Step Guide",
|
||||
"content": "Create a step by step guide to instruct how to [topic].",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_61",
|
||||
"name": "Article Generator",
|
||||
"content": "Write an article about [topic].\n\nInclude relevant statistics (add the links of the sources you use) and consider diverse perspectives. Write it in a [X tone] and mention the source links at the end.",
|
||||
"category": "contentSeo"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_62",
|
||||
"name": "Attention-Interest-Desire-Action",
|
||||
"content": "Using the 'Attention-Interest-Desire-Action' framework, write an email marketing campaign that highlights the\n\n[features]\n\nof our [product/service]\n\nand explains how these [advantages]\n\ncan be helpful to [ideal customer persona].\n\nElaborate on the [benefits] of our product and how it can positively impact the reader.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_63",
|
||||
"name": "Email Subject Generator",
|
||||
"content": "Develop five subject lines for a cold email offering your [product or service] to a potential client",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_64",
|
||||
"name": "Features-Advantages-Benefits",
|
||||
"content": "Using the 'Features-Advantages-Benefits' framework, please write an email marketing campaign that highlights the [features]\n\nof our [product/service]\n\nand explains how these [advantages]\n\ncan be helpful to [ideal customer persona].\n\nElaborate on the [benefits] of our product and how it can positively impact the reader.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_65",
|
||||
"name": "Newsletter From Recent News",
|
||||
"content": "Find recent news about [topic or event] and then turn it into a long form newsletter consisting of a subject line, intro, body paragraph, bullet point list, and a conclusion.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_66",
|
||||
"name": "Newsletter Inspiration",
|
||||
"content": "What are the top trends in [industry] that I can include in my next newsletter focused on [details about your newsletter]?",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_67",
|
||||
"name": "Pain-Agitate-Solution",
|
||||
"content": "Using the 'Pain-Agitate-Solution' framework, please write an email marketing campaign that highlights the [features]\n\nof our [product/service]\n\nand explains how these [advantages]\n\ncan be helpful to [ideal customer persona].\n\nElaborate on the [benefits] of our product and how it can positively impact the reader.",
|
||||
"category": "emailMarketing"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_68",
|
||||
"name": "Facebook Ad",
|
||||
"content": "Create 3 variations of effective ad copy to promote [product] for [audience].\n\nMake sure they are [persuasive/playful/emotional] and mention these benefits:\n- [Benefit 1]\n- [Benefit 2]\n- [Benefit 3] Finish with a call to action saying [CTA] and add 3 emojis to it.",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_69",
|
||||
"name": "Facebook Ad (PAS)",
|
||||
"content": "Product Description: [Product Description]\n\nWrite a PAS for the product\n\nConvert the Problem Agitate Solution into Facebook ad copy\n\nWrite a Facebook ad headline",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_70",
|
||||
"name": "Facebook Headlines",
|
||||
"content": "Brainstorm 20 compelling headlines for a Facebook ad promoting [product description] for [audience]. Format the output as a table.",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_71",
|
||||
"name": "Facebook Video Script",
|
||||
"content": "Write a Facebook ad video script for [product description] that speaks directly to [our target audience] and addresses their pain points and desires",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_72",
|
||||
"name": "Google Ads",
|
||||
"content": "Create 10 google ads (a headline and a description) for [product description] targeting the keyword [keyword].\n\nThe headline of the ad needs to be under 30 characters. The description needs to be under 90 characters. Format the output as a table.",
|
||||
"category": "paidAds"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_73",
|
||||
"name": "Customer Case Study",
|
||||
"content": "Write a customer case study highlighting how [company name] used [product name] to [achieve success]. Include 4 customer quotes, 2 customer success metrics, and visual elements.\n\nCase details:\n[additional details go here]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_74",
|
||||
"name": "Event Invite",
|
||||
"content": "Write a persuasive email to increase attendance at our upcoming event about [theme].\n\nHere are additional details about the event to include:\n[event details]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_75",
|
||||
"name": "Internal Memos",
|
||||
"content": "Write an internal memo to [specific department/team] regarding [topic].\n\nIn the memo, explain [key details] and the desired outcome. Provide an action plan with clear steps and timeline.",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_76",
|
||||
"name": "Pitch a Journalist",
|
||||
"content": "Pitch a story to [reporter name/publication].\n\nStory details: [details]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_77",
|
||||
"name": "Press Release",
|
||||
"content": "Write a press release for [company or organization] announcing a recent achievement or milestone. Begin with a concise and attention-grabbing headline that summarizes the main news. In the opening paragraph, provide a brief overview of the announcement and its significance. In the following paragraphs, provide more details about the achievement, including any relevant statistics or data. Use quotes from company representatives or experts to add credibility and context to the announcement. End the press release with a brief company description and contact information for media inquiries.\n\nRemember to keep the press release concise, informative, and engaging, highlighting the key takeaways and benefits of the announcement.\n\nPress release details:\n\n[paste any necessary details about the announcement here]",
|
||||
"category": "prCommunication"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_78",
|
||||
"name": "Linkedin Connection Invite Message",
|
||||
"content": "You are a recruiter trying to attract top talent.\n\nYou came across this linkedin profile [linkedin URL].\n\nYou have to pitch them and you can't write more than 500 characters\n\nYour message should include:\n- the name of the company they're currently working for\n- praise for their areas of expertise",
|
||||
"category": "recruiting"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_79",
|
||||
"name": "3 Step Outreach Sequence",
|
||||
"content": "Generate a 3 step outreach sequence for [company URL] selling to [target customer]",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_80",
|
||||
"name": "Analyze Industry Trends",
|
||||
"content": "Analyze the industry trends for [industry] in the [country/region] for the past 12 months and compare it to the same period last year.\n\nPresent your findings in a report.",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_81",
|
||||
"name": "Brainstorm Pain Points",
|
||||
"content": "Act as a [target persona].\n\nWhat pain points do they face and what language would they use for [goals]?",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_82",
|
||||
"name": "Competitive Analysis",
|
||||
"content": "Conduct a full analysis of [competitor company name] and identify the competitive advantages and disadvantages of their product.",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_83",
|
||||
"name": "Linkedin Boolean Search",
|
||||
"content": "Write me a boolean search on variations of [roles] that I can plug into sales navigator search",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_84",
|
||||
"name": "Personalized Cold Email From LinkedIn Profile",
|
||||
"content": "Write a personalized cold email to [linkedin profile url] selling [product description].\n\nMake sure the first 7 words of the email catch the reader's attention. Make the email 4-6 sentences.",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "built_in_prompt_85",
|
||||
"name": "Research Prospect From Linkedin",
|
||||
"content": "Summarize [linkedin profile url] into 10 bullet points, brainstorm the pain points they have around [topic]",
|
||||
"category": "sales"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_86",
|
||||
"name": "Caption Generator",
|
||||
"content": "Write a catchy caption about [your theme] and try to play with words to make it fun, engage users in the end, ask them questions, use relevant emojis, and 3 hashtags in the end",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_87",
|
||||
"name": "Generate Content Calendar",
|
||||
"content": "Generate a content calendar about [topic]",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_88",
|
||||
"name": "Headlines",
|
||||
"content": "Write 5 attention-grabbing headlines for a [platform] post on [topic] for [audience].",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_89",
|
||||
"name": "Instagram Captions",
|
||||
"content": "Write 5 variations of Instagram captions for [product].\n\nUse friendly, human-like language that appeals to [target audience].\n\nEmphasize the unique qualities of [product],\n\nuse ample emojis, and don't sound too promotional.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_90",
|
||||
"name": "LinkedIn Post",
|
||||
"content": "Create a narrative Linkedin post using immersive writing about [topic].\n\nDetails:\n\n[give details in bullet point format]\n\nUse a mix of short and long sentences. Make it punchy and dramatic.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_91",
|
||||
"name": "TikTok Script",
|
||||
"content": "Write a super engaging TikTok video script on [topic]. Each sentence should catch the viewer's attention to make them keep watching.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_92",
|
||||
"name": "Twitter Thread",
|
||||
"content": "Give a controversial opinion on [topic], then turn it into a twitter thread.",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_93",
|
||||
"name": "Youtube Video Description",
|
||||
"content": "Write a 100-word YouTube video description that compels [audience]\n\nto watch a video on [topic]\n\nand mentions the following keywords\n\n[keyword 1]\n\n[keyword 2]\n\n[keyword 3].",
|
||||
"category": "socialMedia"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_94",
|
||||
"name": "Reframing Business Perspectives",
|
||||
"content": "Apply Reframing Business Perspectives to analyze [my business decision]. Look at the problem from different angles, challenging the existing beliefs.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_95",
|
||||
"name": "Behavioral Economics Principles",
|
||||
"content": "Apply Behavioral Economics Principles to analyze [my business decision]. Consider how cognitive, emotional, and social factors affect economic decisions.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_96",
|
||||
"name": "Blue Ocean Strategy",
|
||||
"content": "Apply the Blue Ocean Strategy to evaluate [my business decision]. Focus on creating uncontested market space rather than competing in existing industries.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_97",
|
||||
"name": "Brand Ecosystem Development",
|
||||
"content": "Utilize Brand Ecosystem Development to assess [my business decision]. Build a network of interrelated products, services, and stakeholders.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_98",
|
||||
"name": "Consumer Behavior Analysis",
|
||||
"content": "Use Consumer Behavior Analysis to evaluate [my business decision]. Dive into the psychological, personal, and social influences on consumer choices.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_99",
|
||||
"name": "Cost-Benefit Analysis",
|
||||
"content": "Apply Cost-Benefit Analysis to assess [my business decision]. Analyze the expected balance of benefits and costs, including possible risk and uncertainties.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_100",
|
||||
"name": "Customer Lifetime Value",
|
||||
"content": "Assess [my business decision] by considering Customer Lifetime Value. Analyze the long-term value of customers to understand how acquisition, retention, and monetization strategies align.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_101",
|
||||
"name": "Customer Persona Building",
|
||||
"content": "Utilize Customer Persona Building to evaluate [my business decision]. Define specific customer archetypes with detailed attributes and needs.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_102",
|
||||
"name": "Disruptive Innovation",
|
||||
"content": "Apply Disruptive Innovation to assess [my business decision]. Consider how groundbreaking changes in technology or methodology might impact your industry or market.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_103",
|
||||
"name": "Double Loop Learning",
|
||||
"content": "Use Double Loop Learning to evaluate [my business decision]. Reflect not just on solutions, but on underlying assumptions and beliefs, encouraging adaptive learning.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_104",
|
||||
"name": "Eisenhower Matrix",
|
||||
"content": "Use the Eisenhower Matrix to evaluate [my business decision]. Categorize tasks or elements based on urgency and importance to prioritize effectively.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_105",
|
||||
"name": "Emotional Intelligence",
|
||||
"content": "Evaluate [my business decision] with Emotional Intelligence in mind. Recognize and manage both your own and others' emotions to make more empathetic and effective decisions.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_106",
|
||||
"name": "Freemium Business Model",
|
||||
"content": "Apply the Freemium Business Model to [my business decision]. Explore offering basic services for free while charging for premium features.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_107",
|
||||
"name": "Heuristics and Decision Trees",
|
||||
"content": "Evaluate [my business decision] using Heuristics and Decision Trees. Create simplified models to understand complex problems and find optimal paths.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_108",
|
||||
"name": "Hyper-Personalization Strategy",
|
||||
"content": "Use Hyper-Personalization Strategy to evaluate [my business decision]. Leverage data and AI to provide an extremely personalized experience.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_109",
|
||||
"name": "Innovation Ambition Matrix",
|
||||
"content": "Evaluate [my business decision] through the Innovation Ambition Matrix. Plot initiatives on a matrix to balance core enhancements, adjacent opportunities, and transformational innovations.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_110",
|
||||
"name": "Jobs to Be Done Framework",
|
||||
"content": "Assess [my business decision] by applying the Jobs to Be Done Framework. Focus on the problems customers are trying to solve.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_111",
|
||||
"name": "Kano Model Analysis",
|
||||
"content": "Evaluate [my business decision] using the Kano Model Analysis. Prioritize customer needs into basic, performance, and excitement categories.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_112",
|
||||
"name": "Lean Startup Principles",
|
||||
"content": "Apply Lean Startup Principles to [my business decision]. Focus on creating a minimum viable product, measuring its success, and learning from the results.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_113",
|
||||
"name": "Long Tail Strategy",
|
||||
"content": "Analyze [my business decision] focusing on the Long Tail Strategy. Consider how niche markets or products may contribute to overall success.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_114",
|
||||
"name": "Network Effects",
|
||||
"content": "Analyze [my business decision] through the understanding of Network Effects. Consider how the value of a product or service increases as more people use it.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_115",
|
||||
"name": "Pre-Mortem Analysis",
|
||||
"content": "Utilize Pre-Mortem Analysis to assess [my business decision]. Imagine a future failure of the decision and work backward to identify potential causes and mitigation strategies.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_116",
|
||||
"name": "Prospect Theory",
|
||||
"content": "Utilize Prospect Theory to assess [my business decision]. Understand how people perceive gains and losses and how that can influence decision-making.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_117",
|
||||
"name": "Pygmalion Effect",
|
||||
"content": "Apply the Pygmalion Effect to analyze [my business decision]. Recognize how expectations can influence outcomes, both positively and negatively.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_118",
|
||||
"name": "Resource-Based View",
|
||||
"content": "Apply the Resource-Based View to evaluate [my business decision]. Focus on leveraging the company's internal strengths and weaknesses in relation to external opportunities and threats.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_119",
|
||||
"name": "Risk-Reward Analysis",
|
||||
"content": "Analyze [my business decision] through Risk-Reward Analysis. Evaluate the potential risks against the potential rewards to understand the balance and make an informed decision.",
|
||||
"category": "strategy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_120",
|
||||
"name": "Focus on Results",
|
||||
"content": "Can you provide me with a case study template for [CLIENT NAME]'s [PRODUCT/SERVICE] that showcases the results achieved? Please suggest the key metrics and KPIs that should be included. Use [AGENCY NAME], [RESULTS], and [KEY METRICS] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_121",
|
||||
"name": "Showcase Digital Transformation",
|
||||
"content": "How would you structure a case study for [CLIENT NAME] that focuses on their journey of digital transformation? Please suggest the main sections, such as the challenges, the solutions, and the outcomes achieved. Use [AGENCY NAME], [CLIENT NAME], and [OUTCOMES] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_122",
|
||||
"name": "Highlighting Collaboration",
|
||||
"content": "Can you generate a case study template for [CLIENT NAME] that highlights their partnership with [PARTNER NAME]? Please include details on the collaboration process, the benefits for both parties, and the outcomes achieved. Use [AGENCY NAME], [CLIENT NAME], and [PARTNER NAME] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_123",
|
||||
"name": "Achieving Goals",
|
||||
"content": "How would you structure a case study for [CLIENT NAME] that showcases their success in achieving sustainability goals? Please suggest the main sections, such as the challenges, the solutions, and the impact achieved. Use [AGENCY NAME], [CLIENT NAME], and [IMPACT] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_124",
|
||||
"name": "Focus on Strategy",
|
||||
"content": "Can you provide me with a case study template for [CLIENT NAME] that highlights their social media strategy? Please include details on the objectives, the tactics, and the results achieved. Use [AGENCY NAME], [CLIENT NAME], and [RESULTS] as placeholders for customization.",
|
||||
"category": "caseStudies"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_125",
|
||||
"name": "Highlight Unique Features",
|
||||
"content": "Can you write a benefit-driven sales copy that highlights the unique features and benefits of [product/service]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_126",
|
||||
"name": "Explain the Benefits",
|
||||
"content": "How would you describe the benefits of [product/service] to a potential customer in a way that would convince them to purchase?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_127",
|
||||
"name": "Explain the Need",
|
||||
"content": "Can you craft a benefit-driven sales copy that explains why [product/service] is the best solution for [customer's problem or need]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_128",
|
||||
"name": "Crafting Convincing Copy",
|
||||
"content": "How would you sell the benefits of [product/service] to a customer who is on the fence about making a purchase?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_129",
|
||||
"name": "Overcome Objections",
|
||||
"content": "How would you sell the benefits of [product/service] to a customer who is skeptical about its effectiveness for [customer's problem or need]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_130",
|
||||
"name": "Make Your Product Stand Out (Long-Form)",
|
||||
"content": "Can you write an extended sales copy describing the unique features and benefits of [product/service] that sets it apart from the competition and makes it a must-have for [target audience]?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_131",
|
||||
"name": "Top Features (Long-Form)",
|
||||
"content": "Can you write an extended sales copy on what are the top features of [product/service] that [target audience] will love?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_132",
|
||||
"name": "Explaining the Process (Long-Form)",
|
||||
"content": "Can you write an extended sales copy explaining in detail the process of using [product/service] and how it can help [target audience] achieve their goals?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_133",
|
||||
"name": "Differentiating Your Product (Long-Form)",
|
||||
"content": "Can you write an extended sales copy on what makes [product/service] stand out from similar offerings in the market, and why should [target audience] choose it over the competition?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_134",
|
||||
"name": "Convincing Sales Copy (Long-Form)",
|
||||
"content": "Can you write an extended sales copy convincing me why [product/service] is the missing piece [target audience] needs to take their [aspect of life/business] to the next level?",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_135",
|
||||
"name": "Engaging Sales Pitches (Medium-Form)",
|
||||
"content": "Write a sales pitch for a [product/service] that highlights its unique features and benefits.",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_136",
|
||||
"name": "Persuasive Emails (Medium-Form)",
|
||||
"content": "Write a persuasive email to a potential customer explaining why they should choose [company name] for their [product/service] needs.",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_137",
|
||||
"name": "Persuasive Blog Posts (Medium-Form)",
|
||||
"content": "Write a persuasive blog post about the benefits of using [product/service] for [specific industry/target market].",
|
||||
"category": "salesCopy"
|
||||
},
|
||||
{
|
||||
"id": "builtin_prompt_138",
|
||||
"name": "Problem-Solving Sales Pitches (Medium-Form)",
|
||||
"content": "Write a sales pitch for a [product or service] that emphasizes its unique features and benefits. Include specific details about how it can solve a problem or improve the customer's life.",
|
||||
"category": "salesCopy"
|
||||
}
|
||||
]
|
||||
}
|
||||
Whitespace-only changes.
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM9.25 3.75C9.25 4.44036 8.69036 5 8 5C7.30964 5 6.75 4.44036 6.75 3.75C6.75 3.05964 7.30964 2.5 8 2.5C8.69036 2.5 9.25 3.05964 9.25 3.75ZM12 8H9.41901L11.2047 13H9.081L8 9.97321L6.91901 13H4.79528L6.581 8H4V6H12V8Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 617 B |
File diff suppressed because it is too large.
Load diff
@@ -1,12 +0,0 @@
|
||||
output: dist/
|
||||
releases:
|
||||
- name: dev
|
||||
jobs:
|
||||
- name: release-dev-linux-deb
|
||||
package:
|
||||
platform: linux
|
||||
target: deb
|
||||
- name: release-dev-linux-rpm
|
||||
package:
|
||||
platform: linux
|
||||
target: rpm
|
||||
@@ -1,36 +0,0 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT
|
||||
rrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG
|
||||
4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw
|
||||
+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV
|
||||
KiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5
|
||||
b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z
|
||||
QRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW
|
||||
YWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG
|
||||
G4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu
|
||||
6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA
|
||||
6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp
|
||||
q1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd
|
||||
0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/
|
||||
4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb
|
||||
K13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7
|
||||
hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO
|
||||
s6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz
|
||||
Axp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4
|
||||
uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV
|
||||
Wthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn
|
||||
ixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB
|
||||
+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN
|
||||
C9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r
|
||||
vYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx
|
||||
k6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y
|
||||
GMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/
|
||||
eKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG
|
||||
hCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM
|
||||
EoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8
|
||||
iBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI
|
||||
7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb
|
||||
w6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf
|
||||
1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P
|
||||
Y29SB4jvwqls268rP0cWqy4WXwlVwuc=
|
||||
-----END PUBLIC KEY-----
|
||||
+10
-13
@@ -23,24 +23,24 @@ void main() {
|
||||
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
|
||||
|
||||
// Is expanded by default
|
||||
expect(collapseFinder, findsNothing);
|
||||
expect(expandFinder, findsOneWidget);
|
||||
|
||||
// Collapse hidden groups
|
||||
await tester.tap(expandFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Is collapsed
|
||||
expect(collapseFinder, findsOneWidget);
|
||||
expect(expandFinder, findsNothing);
|
||||
|
||||
// Expand hidden groups
|
||||
// Collapse hidden groups
|
||||
await tester.tap(collapseFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Is expanded
|
||||
// Is collapsed
|
||||
expect(collapseFinder, findsNothing);
|
||||
expect(expandFinder, findsOneWidget);
|
||||
|
||||
// Expand hidden groups
|
||||
await tester.tap(expandFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Is expanded
|
||||
expect(collapseFinder, findsOneWidget);
|
||||
expect(expandFinder, findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('hide first group, and show it again', (tester) async {
|
||||
@@ -48,9 +48,6 @@ void main() {
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
|
||||
|
||||
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
|
||||
await tester.tapButton(expandFinder);
|
||||
|
||||
// Tap the options of the first group
|
||||
final optionsFinder = find
|
||||
.descendant(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'data_migration/data_migration_test_runner.dart'
|
||||
as data_migration_test_runner;
|
||||
import 'database/database_test_runner.dart' as database_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;
|
||||
@@ -28,8 +26,4 @@ Future<void> main() async {
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
|
||||
// database
|
||||
database_test_runner.main();
|
||||
}
|
||||
+7
@@ -15,6 +15,7 @@ void main() {
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
|
||||
await tester.tapContinousAnotherWay();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
@@ -30,6 +31,12 @@ void main() {
|
||||
await tester.enterUserName('local_user');
|
||||
|
||||
// Scroll to sign-in
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(AccountSignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
|
||||
await tester.tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
// sign up with Google
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
import 'anon_user_data_migration_test.dart' as anon_user_test;
|
||||
|
||||
void main() async {
|
||||
// anon_user_test.main();
|
||||
anon_user_test.main();
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
hide UploadImageMenu, ResizableImage;
|
||||
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:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/database_test_op.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// copy link to block
|
||||
group('database image:', () {
|
||||
testWidgets('insert image', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open the first row detail page and upload an image
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Grid,
|
||||
pageName: 'database image',
|
||||
);
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// insert an image block
|
||||
{
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
// upload an image
|
||||
{
|
||||
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');
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
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], isNotEmpty);
|
||||
|
||||
// remove the temp file
|
||||
file.deleteSync();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'database_image_test.dart' as database_image_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
database_image_test.main();
|
||||
}
|
||||
+2
-2
@@ -33,7 +33,7 @@ void main() {
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_aiWriter.tr(),
|
||||
);
|
||||
expect(find.byType(AiWriterBlockComponent), findsOneWidget);
|
||||
expect(find.byType(AIWriterBlockComponent), findsOneWidget);
|
||||
|
||||
// switch to another page
|
||||
await tester.openPage(Constants.gettingStartedPageName);
|
||||
@@ -41,7 +41,7 @@ void main() {
|
||||
await tester.openPage(pageName);
|
||||
|
||||
// expect the ai writer block is not in the document
|
||||
expect(find.byType(AiWriterBlockComponent), findsNothing);
|
||||
expect(find.byType(AIWriterBlockComponent), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ void main() {
|
||||
// move the checkbox to the child of the block at path [9]
|
||||
await tester.editor.dragBlock(
|
||||
[10],
|
||||
const Offset(120, -20),
|
||||
const Offset(80, -30),
|
||||
);
|
||||
|
||||
// wait for the move animation to complete
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
+6
@@ -57,6 +57,12 @@ void main() {
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Scroll to sign-in
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(AccountSignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
await tester.tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ void main() {
|
||||
);
|
||||
|
||||
final shareValues = plainText!
|
||||
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
|
||||
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
|
||||
.split('/');
|
||||
final workspaceId = shareValues[0];
|
||||
expect(workspaceId, isNotEmpty);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/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';
|
||||
@@ -72,16 +71,5 @@ 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
-9
@@ -120,10 +120,6 @@ void main() {
|
||||
widget is PublishedViewItem &&
|
||||
widget.publishInfoView.view.name == pageName,
|
||||
);
|
||||
if (pageItem.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(pageItem, findsOneWidget);
|
||||
|
||||
// comment it out because it's not allowed to update the namespace in free plan
|
||||
@@ -253,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(2000);
|
||||
await tester.wait(1000);
|
||||
|
||||
// check if the page is published in sites page
|
||||
final pageItem = find.byWidgetPredicate(
|
||||
@@ -261,10 +257,6 @@ More actions for published page:
|
||||
widget is PublishedViewItem &&
|
||||
widget.publishInfoView.view.name == pageName,
|
||||
);
|
||||
if (pageItem.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(pageItem, findsOneWidget);
|
||||
|
||||
final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());
|
||||
|
||||
+8
-104
@@ -1,15 +1,6 @@
|
||||
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_cell.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:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -17,9 +8,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Folder Search', () {
|
||||
testWidgets('Search for views', (tester) async {
|
||||
@@ -44,106 +33,21 @@ void main() {
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
|
||||
// Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna)
|
||||
expect(find.byType(SearchResultCell), findsNWidgets(2));
|
||||
expect(find.byType(SearchResultTile), findsNWidgets(2));
|
||||
|
||||
// The score should be higher for "ViewOna" thus it should be shown first
|
||||
final secondDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultCell).first) as SearchResultCell;
|
||||
expect(secondDocumentWidget.item.displayName, secondDocument);
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
expect(secondDocumentWidget.result.data, secondDocument);
|
||||
|
||||
// Change search to "ViewOne"
|
||||
await tester.enterText(searchFieldFinder, firstDocument);
|
||||
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(SearchResultCell).first,
|
||||
) as SearchResultCell;
|
||||
expect(firstDocumentWidget.item.displayName, 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(SearchResultCell), findsNWidgets(2));
|
||||
|
||||
/// check results
|
||||
final svgs = find.descendant(
|
||||
of: find.byType(SearchResultCell),
|
||||
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,
|
||||
);
|
||||
});
|
||||
final firstDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
expect(firstDocumentWidget.result.data, firstDocument);
|
||||
});
|
||||
});
|
||||
}
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -27,12 +27,11 @@ void main() {
|
||||
expect(find.byType(RecentViewsList), findsOneWidget);
|
||||
|
||||
// Expect three recent history items
|
||||
expect(find.byType(SearchRecentViewCell), findsNWidgets(3));
|
||||
expect(find.byType(RecentViewTile), findsNWidgets(3));
|
||||
|
||||
// Expect the first item to be the last viewed document
|
||||
final firstDocumentWidget =
|
||||
tester.widget(find.byType(SearchRecentViewCell).first)
|
||||
as SearchRecentViewCell;
|
||||
tester.widget(find.byType(RecentViewTile).first) as RecentViewTile;
|
||||
expect(firstDocumentWidget.view.name, secondDocument);
|
||||
});
|
||||
});
|
||||
|
||||
-41
@@ -15,7 +15,6 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a database and add a linked database view
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
|
||||
@@ -30,11 +29,6 @@ void main() {
|
||||
await tester.tapHidePropertyButton();
|
||||
tester.noFieldWithName('New field 1');
|
||||
|
||||
// create another field, New field 1 to be hidden still
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.dismissFieldEditor();
|
||||
tester.noFieldWithName('New field 1');
|
||||
|
||||
// go back to inline database view, expect field to be shown
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
tester.findFieldWithName('New field 1');
|
||||
@@ -66,40 +60,5 @@ void main() {
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
|
||||
});
|
||||
|
||||
testWidgets('field cell width', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a database and add a linked database view
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// check the width of the field
|
||||
expect(tester.getFieldWidth('New field 1'), 150);
|
||||
|
||||
// change the width of the field
|
||||
await tester.changeFieldWidth('New field 1', 200);
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
|
||||
// create another field, New field 1 to be same width
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.dismissFieldEditor();
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
|
||||
// go back to inline database view, expect New field 1 to be 150px
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
expect(tester.getFieldWidth('New field 1'), 150);
|
||||
|
||||
// go back to linked database view, expect New field 1 to be 205px
|
||||
await tester.tapTabBarLinkedViewByViewName('Grid');
|
||||
expect(tester.getFieldWidth('New field 1'), 205);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.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/shared/sidebar_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.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/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('change icon', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
const pageName = 'Database';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
layout: ViewLayoutPB.Grid,
|
||||
name: pageName,
|
||||
);
|
||||
|
||||
/// create board
|
||||
final addButton = find.byType(AddDatabaseViewButton);
|
||||
await tester.tapButton(addButton);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Board.layoutName}',
|
||||
findRichText: true,
|
||||
),
|
||||
);
|
||||
|
||||
/// create calendar
|
||||
await tester.tapButton(addButton);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Calendar.layoutName}',
|
||||
findRichText: true,
|
||||
),
|
||||
);
|
||||
|
||||
final databaseTabBarItem = find.byType(DatabaseTabBarItem);
|
||||
expect(databaseTabBarItem, findsNWidgets(3));
|
||||
final gridItem = databaseTabBarItem.first,
|
||||
boardItem = databaseTabBarItem.at(1),
|
||||
calendarItem = databaseTabBarItem.last;
|
||||
|
||||
/// change the icon of grid
|
||||
/// the first tapping is to select specific item
|
||||
/// the second tapping is to show the menu
|
||||
await tester.tapButton(gridItem);
|
||||
await tester.tapButton(gridItem);
|
||||
|
||||
/// change icon
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final gridIcon = find.descendant(
|
||||
of: gridItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final gridIconWidget =
|
||||
gridIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final iconsData = IconsData.fromJson(jsonDecode(iconData.emoji));
|
||||
final gridIconsData =
|
||||
IconsData.fromJson(jsonDecode(gridIconWidget.emoji.emoji));
|
||||
expect(gridIconsData.iconName, iconsData.iconName);
|
||||
|
||||
/// change the icon of board
|
||||
await tester.tapButton(boardItem);
|
||||
await tester.tapButton(boardItem);
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final boardIcon = find.descendant(
|
||||
of: boardItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final boardIconWidget =
|
||||
boardIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final boardIconsData =
|
||||
IconsData.fromJson(jsonDecode(boardIconWidget.emoji.emoji));
|
||||
expect(boardIconsData.iconName, iconsData.iconName);
|
||||
|
||||
/// change the icon of calendar
|
||||
await tester.tapButton(calendarItem);
|
||||
await tester.tapButton(calendarItem);
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final calendarIcon = find.descendant(
|
||||
of: calendarItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final calendarIconWidget =
|
||||
calendarIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final calendarIconsData =
|
||||
IconsData.fromJson(jsonDecode(calendarIconWidget.emoji.emoji));
|
||||
expect(calendarIconsData.iconName, iconsData.iconName);
|
||||
});
|
||||
|
||||
testWidgets('change database icon from sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
final icon = IconsData.fromJson(jsonDecode(iconData.emoji)), emoji = '😄';
|
||||
|
||||
const pageName = 'Database';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
layout: ViewLayoutPB.Grid,
|
||||
name: pageName,
|
||||
);
|
||||
final viewItem = find.descendant(
|
||||
of: find.byType(SidebarFolder),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is ViewItem && w.view.name == pageName,
|
||||
),
|
||||
);
|
||||
|
||||
/// change icon to emoji
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: viewItem,
|
||||
matching: find.byType(FlowySvg),
|
||||
),
|
||||
);
|
||||
await tester.tapEmoji(emoji);
|
||||
final iconWidget = find.descendant(
|
||||
of: viewItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
expect(
|
||||
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
|
||||
emoji,
|
||||
);
|
||||
|
||||
/// the icon will not be displayed in database item
|
||||
Finder databaseIcon = find.descendant(
|
||||
of: find.byType(DatabaseTabBarItem),
|
||||
matching: find.byType(FlowySvg),
|
||||
);
|
||||
expect(
|
||||
(databaseIcon.evaluate().first.widget as FlowySvg).svg,
|
||||
FlowySvgs.icon_grid_s,
|
||||
);
|
||||
|
||||
/// change emoji to icon
|
||||
await tester.tapButton(iconWidget);
|
||||
await tester.tapIcon(iconData);
|
||||
expect(
|
||||
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
|
||||
iconData.emoji,
|
||||
);
|
||||
|
||||
databaseIcon = find.descendant(
|
||||
of: find.byType(DatabaseTabBarItem),
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final databaseIconWidget =
|
||||
databaseIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final databaseIconsData =
|
||||
IconsData.fromJson(jsonDecode(databaseIconWidget.emoji.emoji));
|
||||
expect(icon.svgString, databaseIconsData.svgString);
|
||||
expect(icon.color, isNotEmpty);
|
||||
expect(icon.color, databaseIconsData.color);
|
||||
|
||||
/// the icon in database item should not show the color
|
||||
expect(databaseIconWidget.enableColor, false);
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -78,37 +73,5 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('insert grid in column', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// create page and show slash menu
|
||||
await tester.createNewPageWithNameUnderParent(name: 'test page');
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// create a column
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_twoColumns.tr(),
|
||||
);
|
||||
final actionList = find.byType(BlockActionList);
|
||||
expect(actionList, findsNWidgets(2));
|
||||
final position = tester.getCenter(actionList.last);
|
||||
|
||||
/// tap the second child of column
|
||||
await tester.tapAt(position.copyWith(dx: position.dx + 50));
|
||||
|
||||
/// create a grid
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_grid.tr(),
|
||||
);
|
||||
|
||||
final grid = find.byType(GridPageContent);
|
||||
expect(grid, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
+7
-10
@@ -27,9 +27,8 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the align center
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
|
||||
await tester
|
||||
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
|
||||
// expect to see the align center
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
@@ -37,15 +36,13 @@ void main() {
|
||||
expect(first.attributes[blockComponentAlign], 'center');
|
||||
|
||||
// click the align right
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
|
||||
await tester
|
||||
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_right_m);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
expect(first.attributes[blockComponentAlign], 'right');
|
||||
|
||||
// click the align left
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
|
||||
await tester
|
||||
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_left_m);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
expect(first.attributes[blockComponentAlign], 'left');
|
||||
});
|
||||
|
||||
@@ -78,7 +75,7 @@ void main() {
|
||||
[
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.keyC,
|
||||
LogicalKeyboardKey.keyE,
|
||||
],
|
||||
tester: tester,
|
||||
withKeyUp: true,
|
||||
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
+320
-334
@@ -1,12 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
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';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
@@ -21,7 +19,7 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('copy and paste in document:', () {
|
||||
group('copy and paste in document', () {
|
||||
testWidgets('paste multiple lines at the first line', (tester) async {
|
||||
// mock the clipboard
|
||||
const lines = 3;
|
||||
@@ -174,316 +172,305 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
|
||||
await tester.pasteContent(
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('png', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(jpeg) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(gif) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.gif');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('gif', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'format the selected text to href when pasting url if available',
|
||||
(tester) async {
|
||||
const text = 'appflowy';
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(
|
||||
plainText: url,
|
||||
beforeTest: (editorState) async {
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': text,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: bordeLine truncated
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
final pasteAsMenu = find.byType(PasteAsMenu);
|
||||
expect(pasteAsMenu, findsOneWidget);
|
||||
final bookmarkButton = find.text(
|
||||
LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),
|
||||
);
|
||||
await tester.tapButton(bookmarkButton);
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
// hover on the link preview block
|
||||
// click the more button
|
||||
// and select convert to link
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(CustomLinkPreviewWidget),
|
||||
onHover: () async {
|
||||
/// show menu
|
||||
final menu = find.byType(CustomLinkPreviewMenu);
|
||||
expect(menu, findsOneWidget);
|
||||
await tester.tapButton(menu);
|
||||
|
||||
final convertToLinkButton = find.text(
|
||||
LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl
|
||||
.tr(),
|
||||
);
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tapButton(convertToLinkButton);
|
||||
},
|
||||
);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final textNode = editorState.getNodeAtPath([0])!;
|
||||
expect(textNode.type, ParagraphBlockKeys.type);
|
||||
expect(textNode.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'ctrl/cmd+z to undo the auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
final pasteAsMenu = find.byType(PasteAsMenu);
|
||||
expect(pasteAsMenu, findsOneWidget);
|
||||
final bookmarkButton = find.text(
|
||||
LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),
|
||||
);
|
||||
await tester.tapButton(bookmarkButton);
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'paste the nodes start with non-delta node',
|
||||
(tester) async {
|
||||
await tester.pasteContent((_) {});
|
||||
const text = 'Hello World';
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
await tester.pasteContent(
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
// [image_block]
|
||||
// [paragraph_block]
|
||||
transaction.insertNodes([
|
||||
0,
|
||||
], [
|
||||
customImageNode(url: ''),
|
||||
paragraphNode(text: text),
|
||||
]);
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// select all and copy
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
// put the cursor to the end of the paragraph block
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// paste the content
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect the image and the paragraph block are inserted below the cursor
|
||||
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste the url without protocol', (tester) async {
|
||||
// paste the image that from local file
|
||||
const plainText = '1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('png', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the image url', (tester) async {
|
||||
const plainText = 'http://example.com/1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
testWidgets('paste image(jpeg) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
testWidgets('paste image(gif) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.gif');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('gif', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'format the selected text to href when pasting url if available',
|
||||
(tester) async {
|
||||
const text = 'appflowy';
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(
|
||||
plainText: url,
|
||||
beforeTest: (editorState) async {
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': text,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
// hover on the link preview block
|
||||
// click the more button
|
||||
// and select convert to link
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(CustomLinkPreviewWidget),
|
||||
onHover: () async {
|
||||
final convertToLinkButton = find.byWidgetPredicate((widget) {
|
||||
return widget is MenuBlockButton &&
|
||||
widget.tooltip ==
|
||||
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
|
||||
});
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tap(convertToLinkButton);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final textNode = editorState.getNodeAtPath([0])!;
|
||||
expect(textNode.type, ParagraphBlockKeys.type);
|
||||
expect(textNode.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'ctrl/cmd+z to undo the auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'paste the nodes start with non-delta node',
|
||||
(tester) async {
|
||||
await tester.pasteContent((_) {});
|
||||
const text = 'Hello World';
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final transaction = editorState.transaction;
|
||||
// [image_block]
|
||||
// [paragraph_block]
|
||||
transaction.insertNodes([
|
||||
0,
|
||||
], [
|
||||
customImageNode(url: ''),
|
||||
paragraphNode(text: text),
|
||||
]);
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// select all and copy
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
// put the cursor to the end of the paragraph block
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// paste the content
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect the image and the paragraph block are inserted below the cursor
|
||||
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the url without protocol', (tester) async {
|
||||
// paste the image that from local file
|
||||
const plainText = '1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the image url', (tester) async {
|
||||
const plainText = 'https://appflowy.io/1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
# I'm h1
|
||||
## I'm h2
|
||||
### I'm h3
|
||||
@@ -491,41 +478,40 @@ void main() {
|
||||
##### 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', (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);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
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 {
|
||||
Future<void> pasteContent(
|
||||
FutureOr<void> Function(EditorState editorState) test, {
|
||||
void Function(EditorState editorState) test, {
|
||||
Future<void> Function(EditorState editorState)? beforeTest,
|
||||
String? plainText,
|
||||
String? html,
|
||||
@@ -537,7 +523,7 @@ extension on WidgetTester {
|
||||
await tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await createNewPageWithNameUnderParent();
|
||||
await createNewPageWithNameUnderParent(name: 'Test Document');
|
||||
// tap the editor
|
||||
await tapButton(find.byType(AppFlowyEditor));
|
||||
|
||||
@@ -560,8 +546,8 @@ extension on WidgetTester {
|
||||
isShiftPressed: pasteAsPlain,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await pumpAndSettle(const Duration(milliseconds: 1000));
|
||||
await pumpAndSettle();
|
||||
|
||||
await test(editor.getCurrentEditorState());
|
||||
test(editor.getCurrentEditorState());
|
||||
}
|
||||
}
|
||||
-2
@@ -13,8 +13,6 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final finder = find.text(gettingStarted, findRichText: true);
|
||||
await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2));
|
||||
|
||||
// create a new document
|
||||
const pageName = 'Test Document';
|
||||
|
||||
-453
@@ -1,453 +0,0 @@
|
||||
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/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart';
|
||||
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/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const avaliableLink = 'https://appflowy.io/',
|
||||
unavailableLink = 'www.thereIsNoting.com';
|
||||
|
||||
Future<void> preparePage(WidgetTester tester, {String? pageName}) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: pageName);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
}
|
||||
|
||||
Future<void> pasteLink(WidgetTester tester, String link) async {
|
||||
await getIt<ClipboardService>()
|
||||
.setData(ClipboardServiceData(plainText: link));
|
||||
|
||||
/// paste the link
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> pasteAs(
|
||||
WidgetTester tester,
|
||||
String link,
|
||||
PasteMenuType type, {
|
||||
Duration waitTime = const Duration(milliseconds: 500),
|
||||
}) async {
|
||||
await pasteLink(tester, link);
|
||||
final convertToMentionButton = find.text(type.title);
|
||||
await tester.tapButton(convertToMentionButton);
|
||||
await tester.pumpAndSettle(waitTime);
|
||||
}
|
||||
|
||||
void checkUrl(Node node, String link) {
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': link,
|
||||
'attributes': {'href': link},
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
void checkMention(Node node, String link) {
|
||||
final delta = node.delta!;
|
||||
final insert = (delta.first as TextInsert).text;
|
||||
final attributes = delta.first.attributes;
|
||||
expect(insert, MentionBlockKeys.mentionChar);
|
||||
final mention =
|
||||
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
|
||||
expect(mention[MentionBlockKeys.type], MentionType.externalLink.name);
|
||||
expect(mention[MentionBlockKeys.url], avaliableLink);
|
||||
}
|
||||
|
||||
void checkBookmark(Node node, String link) {
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], link);
|
||||
}
|
||||
|
||||
void checkEmbed(Node node, String link) {
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkEmbedKeys.previewType], LinkEmbedKeys.embed);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], link);
|
||||
}
|
||||
|
||||
group('Paste as URL', () {
|
||||
Future<void> pasteAndTurnInto(
|
||||
WidgetTester tester,
|
||||
String link,
|
||||
String title,
|
||||
) async {
|
||||
await pasteLink(tester, link);
|
||||
final convertToLinkButton = find
|
||||
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
|
||||
await tester.tapButton(convertToLinkButton);
|
||||
|
||||
/// hover link and turn into mention
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(LinkHoverTrigger),
|
||||
onHover: () async {
|
||||
final turnintoButton = find.byFlowySvg(FlowySvgs.turninto_m);
|
||||
await tester.tapButton(turnintoButton);
|
||||
final convertToButton = find.text(title);
|
||||
await tester.tapButton(convertToButton);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteLink(tester, link);
|
||||
final convertToLinkButton = find
|
||||
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
|
||||
await tester.tapButton(convertToLinkButton);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link and turn into mention', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAndTurnInto(
|
||||
tester,
|
||||
link,
|
||||
LinkConvertMenuCommand.toMention.title,
|
||||
);
|
||||
|
||||
/// check metion values
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link and turn into bookmark', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAndTurnInto(
|
||||
tester,
|
||||
link,
|
||||
LinkConvertMenuCommand.toBookmark.title,
|
||||
);
|
||||
|
||||
/// check metion values
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link and turn into embed', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAndTurnInto(
|
||||
tester,
|
||||
link,
|
||||
LinkConvertMenuCommand.toEmbed.title,
|
||||
);
|
||||
|
||||
/// check metion values
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
});
|
||||
|
||||
group('Paste as Mention', () {
|
||||
Future<void> pasteAsMention(WidgetTester tester, String link) =>
|
||||
pasteAs(tester, link, PasteMenuType.mention);
|
||||
|
||||
String getMentionLink(Node node) {
|
||||
final insert = node.delta?.first as TextInsert?;
|
||||
final mention = insert?.attributes?[MentionBlockKeys.mention]
|
||||
as Map<String, dynamic>?;
|
||||
return mention?[MentionBlockKeys.url] ?? '';
|
||||
}
|
||||
|
||||
Future<void> hoverMentionAndClick(
|
||||
WidgetTester tester,
|
||||
String command,
|
||||
) async {
|
||||
final mentionLink = find.byType(MentionLinkBlock);
|
||||
expect(mentionLink, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
mentionLink,
|
||||
onHover: () async {
|
||||
final errorPreview = find.byType(MentionLinkErrorPreview);
|
||||
expect(errorPreview, findsOneWidget);
|
||||
final convertButton = find.byFlowySvg(FlowySvgs.turninto_m);
|
||||
await tester.tapButton(convertButton);
|
||||
final menuButton = find.text(command);
|
||||
await tester.tapButton(menuButton);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link as mention', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as mention and copy link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
final mentionLink = find.byType(MentionLinkBlock);
|
||||
expect(mentionLink, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
mentionLink,
|
||||
onHover: () async {
|
||||
final preview = find.byType(MentionLinkPreview);
|
||||
if (!preview.hasFound) {
|
||||
final copyButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(copyButton);
|
||||
} else {
|
||||
final moreOptionButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
final copyButton =
|
||||
find.text(MentionLinktMenuCommand.copyLink.title);
|
||||
await tester.tapButton(copyButton);
|
||||
}
|
||||
},
|
||||
);
|
||||
final clipboardContent = await getIt<ClipboardService>().getData();
|
||||
expect(clipboardContent.plainText, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and turninto url', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.toURL.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and turninto embed', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.toEmbed.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and turninto bookmark', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.toBookmark.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste as error mention and remove link', (tester) async {
|
||||
String link = unavailableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsMention(tester, link);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
link = getMentionLink(node);
|
||||
await hoverMentionAndClick(
|
||||
tester,
|
||||
MentionLinktErrorMenuCommand.removeLink.title,
|
||||
);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{'insert': link},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('Paste as Bookmark', () {
|
||||
Future<void> pasteAsBookmark(WidgetTester tester, String link) =>
|
||||
pasteAs(tester, link, PasteMenuType.bookmark);
|
||||
|
||||
Future<void> hoverAndClick(
|
||||
WidgetTester tester,
|
||||
LinkPreviewMenuCommand command,
|
||||
) async {
|
||||
final bookmark = find.byType(CustomLinkPreviewBlockComponent);
|
||||
expect(bookmark, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
bookmark,
|
||||
onHover: () async {
|
||||
final menuButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
|
||||
await tester.tapButton(menuButton);
|
||||
final commandButton = find.text(command.title);
|
||||
await tester.tapButton(commandButton);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link as bookmark', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to mention',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToMention);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to url', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToUrl);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to embed',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToEmbed);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and copy link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.copyLink);
|
||||
final clipboardContent = await getIt<ClipboardService>().getData();
|
||||
expect(clipboardContent.plainText, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and replace link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.replace);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
|
||||
await tester.enterText(find.byType(TextFormField), unavailableLink);
|
||||
await tester.tapButton(find.text(LocaleKeys.button_replace.tr()));
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, unavailableLink);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and remove link', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsBookmark(tester, link);
|
||||
await hoverAndClick(tester, LinkPreviewMenuCommand.removeLink);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{'insert': link},
|
||||
]);
|
||||
});
|
||||
});
|
||||
group('Paste as Embed', () {
|
||||
Future<void> pasteAsEmbed(WidgetTester tester, String link) =>
|
||||
pasteAs(tester, link, PasteMenuType.embed);
|
||||
|
||||
Future<void> hoverAndConvert(
|
||||
WidgetTester tester,
|
||||
LinkEmbedConvertCommand command,
|
||||
) async {
|
||||
final embed = find.byType(LinkEmbedBlockComponent);
|
||||
expect(embed, findsOneWidget);
|
||||
await tester.hoverOnWidget(
|
||||
embed,
|
||||
onHover: () async {
|
||||
final menuButton = find.byFlowySvg(FlowySvgs.turninto_m);
|
||||
await tester.tapButton(menuButton);
|
||||
final commandButton = find.text(command.title);
|
||||
await tester.tapButton(commandButton);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('paste a link as embed', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkEmbed(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to mention',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to url', (tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
|
||||
testWidgets('paste a link as bookmark and convert to bookmark',
|
||||
(tester) async {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
});
|
||||
}
|
||||
-106
@@ -1,10 +1,4 @@
|
||||
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';
|
||||
|
||||
@@ -37,104 +31,4 @@ 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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
+8
-6
@@ -76,12 +76,13 @@ void main() {
|
||||
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.editor_bulletedListShortForm.tr():
|
||||
LocaleKeys.document_slashMenu_name_bulletedList.tr():
|
||||
BulletedListBlockKeys.type,
|
||||
LocaleKeys.editor_numberedListShortForm.tr():
|
||||
LocaleKeys.document_slashMenu_name_numberedList.tr():
|
||||
NumberedListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
|
||||
LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_todoList.tr():
|
||||
TodoListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
|
||||
};
|
||||
@@ -116,12 +117,13 @@ void main() {
|
||||
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
|
||||
LocaleKeys.editor_bulletedListShortForm.tr():
|
||||
LocaleKeys.document_slashMenu_name_bulletedList.tr():
|
||||
BulletedListBlockKeys.type,
|
||||
LocaleKeys.editor_numberedListShortForm.tr():
|
||||
LocaleKeys.document_slashMenu_name_numberedList.tr():
|
||||
NumberedListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
|
||||
LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_todoList.tr():
|
||||
TodoListBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
|
||||
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
|
||||
};
|
||||
|
||||
-37
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.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';
|
||||
|
||||
@@ -48,41 +47,5 @@ void main() {
|
||||
|
||||
expect(editorState.selection!.start.offset, 0);
|
||||
});
|
||||
|
||||
testWidgets('select and delete text', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
/// input text
|
||||
final editor = tester.editor;
|
||||
final editorState = editor.getCurrentEditorState();
|
||||
|
||||
const inputText = 'Test for text selection and deletion';
|
||||
final texts = inputText.split(' ');
|
||||
await editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(inputText);
|
||||
|
||||
/// selecte and delete
|
||||
int index = 0;
|
||||
while (texts.isNotEmpty) {
|
||||
final text = texts.removeAt(0);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: index),
|
||||
end: Position(path: [0], offset: index + text.length),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
|
||||
index++;
|
||||
}
|
||||
|
||||
/// excpete the text value is correct
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
final nodeText = node.delta?.toPlainText() ?? '';
|
||||
expect(nodeText, ' ' * (index - 1));
|
||||
});
|
||||
});
|
||||
}
|
||||
+58
-48
@@ -1,9 +1,7 @@
|
||||
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';
|
||||
@@ -13,7 +11,6 @@ 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:
|
||||
@@ -40,14 +37,7 @@ import '../../shared/util.dart';
|
||||
const _defaultPageName = "";
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Document SubPageBlock tests', () {
|
||||
testWidgets('Insert a new SubPageBlock from Slash menu items',
|
||||
@@ -58,6 +48,11 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsNWidgets(3),
|
||||
@@ -72,6 +67,12 @@ 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));
|
||||
|
||||
@@ -90,6 +91,11 @@ 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));
|
||||
|
||||
@@ -138,6 +144,11 @@ 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));
|
||||
|
||||
@@ -191,6 +202,11 @@ 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));
|
||||
|
||||
@@ -227,6 +243,11 @@ 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));
|
||||
|
||||
@@ -272,6 +293,11 @@ 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));
|
||||
|
||||
@@ -310,6 +336,11 @@ 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));
|
||||
|
||||
@@ -353,6 +384,11 @@ 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);
|
||||
@@ -375,6 +411,12 @@ 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));
|
||||
|
||||
@@ -395,6 +437,11 @@ 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]);
|
||||
@@ -451,43 +498,6 @@ 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
-2
@@ -13,7 +13,6 @@ 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;
|
||||
import 'document_link_preview_test.dart' as document_link_preview_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -29,5 +28,4 @@ void main() {
|
||||
document_find_menu_test.main();
|
||||
document_toolbar_test.main();
|
||||
document_with_simple_table_test.main();
|
||||
document_link_preview_test.main();
|
||||
}
|
||||
+15
-335
@@ -1,19 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart';
|
||||
import 'package:appflowy/startup/startup.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';
|
||||
|
||||
@@ -22,33 +8,24 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> selectText(WidgetTester tester, String text) async {
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> prepareForToolbar(WidgetTester tester, String text) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await selectText(tester, text);
|
||||
}
|
||||
|
||||
group('document toolbar:', () {
|
||||
testWidgets('font family', (tester) async {
|
||||
await prepareForToolbar(tester, 'font family');
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
const text = 'font family';
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
|
||||
// tap more options button
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_more_m);
|
||||
// tap the font family button
|
||||
final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey);
|
||||
await tester.tapButton(fontFamilyButton);
|
||||
@@ -69,302 +46,5 @@ void main() {
|
||||
abel,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('heading 1~3', (tester) async {
|
||||
const text = 'heading';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
Future<void> testChangeHeading(
|
||||
FlowySvgData svg,
|
||||
String title,
|
||||
int level,
|
||||
) async {
|
||||
/// tap suggestions item
|
||||
final suggestionsButton = find.byKey(kSuggestionsItemKey);
|
||||
await tester.tapButton(suggestionsButton);
|
||||
|
||||
/// tap item
|
||||
await tester.ensureVisible(find.byFlowySvg(svg));
|
||||
await tester.tapButton(find.byFlowySvg(svg));
|
||||
|
||||
/// check the type of node is [HeadingBlockKeys.type]
|
||||
await selectText(tester, text);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final selection = editorState.selection!;
|
||||
final node = editorState.getNodeAtPath(selection.start.path)!,
|
||||
nodeLevel = node.attributes[HeadingBlockKeys.level]!;
|
||||
expect(node.type, HeadingBlockKeys.type);
|
||||
expect(nodeLevel, level);
|
||||
|
||||
/// show toolbar again
|
||||
await selectText(tester, text);
|
||||
|
||||
/// the text of suggestions item should be changed
|
||||
expect(
|
||||
find.descendant(of: suggestionsButton, matching: find.text(title)),
|
||||
findsOneWidget,
|
||||
);
|
||||
}
|
||||
|
||||
await testChangeHeading(
|
||||
FlowySvgs.type_h1_m,
|
||||
LocaleKeys.document_toolbar_h1.tr(),
|
||||
1,
|
||||
);
|
||||
|
||||
await testChangeHeading(
|
||||
FlowySvgs.type_h2_m,
|
||||
LocaleKeys.document_toolbar_h2.tr(),
|
||||
2,
|
||||
);
|
||||
await testChangeHeading(
|
||||
FlowySvgs.type_h3_m,
|
||||
LocaleKeys.document_toolbar_h3.tr(),
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('toggle 1~3', (tester) async {
|
||||
const text = 'toggle';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
Future<void> testChangeToggle(
|
||||
FlowySvgData svg,
|
||||
String title,
|
||||
int? level,
|
||||
) async {
|
||||
/// tap suggestions item
|
||||
final suggestionsButton = find.byKey(kSuggestionsItemKey);
|
||||
await tester.tapButton(suggestionsButton);
|
||||
|
||||
/// tap item
|
||||
await tester.ensureVisible(find.byFlowySvg(svg));
|
||||
await tester.tapButton(find.byFlowySvg(svg));
|
||||
|
||||
/// check the type of node is [HeadingBlockKeys.type]
|
||||
await selectText(tester, text);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final selection = editorState.selection!;
|
||||
final node = editorState.getNodeAtPath(selection.start.path)!,
|
||||
nodeLevel = node.attributes[ToggleListBlockKeys.level];
|
||||
expect(node.type, ToggleListBlockKeys.type);
|
||||
expect(nodeLevel, level);
|
||||
|
||||
/// show toolbar again
|
||||
await selectText(tester, text);
|
||||
|
||||
/// the text of suggestions item should be changed
|
||||
expect(
|
||||
find.descendant(of: suggestionsButton, matching: find.text(title)),
|
||||
findsOneWidget,
|
||||
);
|
||||
}
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_list_m,
|
||||
LocaleKeys.editor_toggleListShortForm.tr(),
|
||||
null,
|
||||
);
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_h1_m,
|
||||
LocaleKeys.editor_toggleHeading1ShortForm.tr(),
|
||||
1,
|
||||
);
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_h2_m,
|
||||
LocaleKeys.editor_toggleHeading2ShortForm.tr(),
|
||||
2,
|
||||
);
|
||||
|
||||
await testChangeToggle(
|
||||
FlowySvgs.type_toggle_h3_m,
|
||||
LocaleKeys.editor_toggleHeading3ShortForm.tr(),
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('toolbar will not rebuild after click item', (tester) async {
|
||||
const text = 'Test rebuilding';
|
||||
await prepareForToolbar(tester, text);
|
||||
Finder toolbar = find.byType(DesktopFloatingToolbar);
|
||||
Element toolbarElement = toolbar.evaluate().first;
|
||||
final elementHashcode = toolbarElement.hashCode;
|
||||
final boldButton = find.byFlowySvg(FlowySvgs.toolbar_bold_m),
|
||||
underlineButton = find.byFlowySvg(FlowySvgs.toolbar_underline_m),
|
||||
italicButton = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m);
|
||||
|
||||
/// tap format buttons
|
||||
await tester.tapButton(boldButton);
|
||||
await tester.tapButton(underlineButton);
|
||||
await tester.tapButton(italicButton);
|
||||
toolbar = find.byType(DesktopFloatingToolbar);
|
||||
toolbarElement = toolbar.evaluate().first;
|
||||
|
||||
/// check if the toolbar is not rebuilt
|
||||
expect(elementHashcode, toolbarElement.hashCode);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
/// check text formats
|
||||
expect(
|
||||
editorState
|
||||
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.bold),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
editorState
|
||||
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.italic),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
editorState
|
||||
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.underline),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('document toolbar: link', () {
|
||||
String? getLinkFromNode(Node node) {
|
||||
for (final insert in node.delta!) {
|
||||
final link = insert.attributes?.href;
|
||||
if (link != null) return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isPageLink(Node node) {
|
||||
for (final insert in node.delta!) {
|
||||
final isPage = insert.attributes?.isPage;
|
||||
if (isPage == true) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String getNodeText(Node node) {
|
||||
for (final insert in node.delta!) {
|
||||
if (insert is TextInsert) return insert.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
testWidgets('insert link and remove link', (tester) async {
|
||||
const text = 'insert link', link = 'https://test.appflowy.cloud';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
final toolbar = find.byType(DesktopFloatingToolbar);
|
||||
expect(toolbar, findsOneWidget);
|
||||
|
||||
/// tap link button to show CreateLinkMenu
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
final createLinkMenu = find.byType(LinkCreateMenu);
|
||||
expect(createLinkMenu, findsOneWidget);
|
||||
|
||||
/// test esc to close
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
expect(toolbar, findsNothing);
|
||||
|
||||
/// show toolbar again
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await selectText(tester, text);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// insert link
|
||||
final textField = find.descendant(
|
||||
of: createLinkMenu,
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(textField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getLinkFromNode(node), link);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
/// hover link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
final hoverMenu = find.byType(LinkHoverMenu);
|
||||
expect(hoverMenu, findsOneWidget);
|
||||
|
||||
/// copy link
|
||||
final copyButton = find.descendant(
|
||||
of: hoverMenu,
|
||||
matching: find.byFlowySvg(FlowySvgs.toolbar_link_m),
|
||||
);
|
||||
await tester.tapButton(copyButton);
|
||||
final clipboardContent = await getIt<ClipboardService>().getData();
|
||||
final plainText = clipboardContent.plainText;
|
||||
expect(plainText, link);
|
||||
|
||||
/// remove link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getLinkFromNode(node), null);
|
||||
});
|
||||
|
||||
testWidgets('insert link and edit link', (tester) async {
|
||||
const text = 'edit link',
|
||||
link = 'https://test.appflowy.cloud',
|
||||
afterText = '$text after';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
/// tap link button to show CreateLinkMenu
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// search for page and select it
|
||||
final textField = find.descendant(
|
||||
of: find.byType(LinkCreateMenu),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(textField, gettingStarted);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
expect(isPageLink(node), true);
|
||||
expect(getLinkFromNode(node) == link, false);
|
||||
|
||||
/// hover link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
|
||||
/// click edit button to show LinkEditMenu
|
||||
final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);
|
||||
await tester.tapButton(editButton);
|
||||
final linkEditMenu = find.byType(LinkEditMenu);
|
||||
expect(linkEditMenu, findsOneWidget);
|
||||
|
||||
/// change the link text
|
||||
final titleField = find.descendant(
|
||||
of: linkEditMenu,
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(titleField, afterText);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.descendant(of: linkEditMenu, matching: find.text(gettingStarted)),
|
||||
);
|
||||
final linkField = find.ancestor(
|
||||
of: find.text(LocaleKeys.document_toolbar_linkInputHint.tr()),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
|
||||
/// apply the change
|
||||
final applyButton =
|
||||
find.text(LocaleKeys.settings_appearance_documentSettings_apply.tr());
|
||||
await tester.tapButton(applyButton);
|
||||
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(isPageLink(node), false);
|
||||
expect(getLinkFromNode(node), link);
|
||||
expect(getNodeText(node), afterText);
|
||||
});
|
||||
});
|
||||
}
|
||||
-61
@@ -1,23 +1,15 @@
|
||||
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() {
|
||||
@@ -68,59 +60,6 @@ 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();
|
||||
|
||||
+92
@@ -7,16 +7,19 @@ 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';
|
||||
@@ -26,6 +29,58 @@ void main() {
|
||||
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();
|
||||
@@ -76,6 +131,43 @@ void main() {
|
||||
file.deleteSync();
|
||||
});
|
||||
|
||||
testWidgets('insert a gif image from network', (tester) async {
|
||||
await testEmbedImage(
|
||||
tester,
|
||||
'https://www.easygifanimator.net/images/samples/sparkles.gif',
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
+19
-77
@@ -1,11 +1,9 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
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:flowy_infra_ui/style_widget/button.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';
|
||||
|
||||
@@ -34,15 +32,9 @@ void main() {
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the more options button
|
||||
final moreOptionButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_toolbar_moreOptions.tr(),
|
||||
);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton = find.text(
|
||||
LocaleKeys.document_toolbar_equation.tr(),
|
||||
final inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
@@ -85,15 +77,10 @@ void main() {
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the more options button
|
||||
final moreOptionButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_toolbar_moreOptions.tr(),
|
||||
);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton =
|
||||
find.byFlowySvg(FlowySvgs.type_formula_m);
|
||||
var inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
@@ -105,7 +92,17 @@ void main() {
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: 1),
|
||||
);
|
||||
|
||||
await tester.tapButton(moreOptionButton);
|
||||
// expect to the see the inline math equation button is highlighted
|
||||
inlineMathEquationButton = find.descendant(
|
||||
of: find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
),
|
||||
matching: find.byType(SVGIconItemWidget),
|
||||
);
|
||||
expect(
|
||||
tester.widget<SVGIconItemWidget>(inlineMathEquationButton).isHighlight,
|
||||
isTrue,
|
||||
);
|
||||
|
||||
// cancel the format
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
@@ -136,15 +133,10 @@ void main() {
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
|
||||
);
|
||||
|
||||
// tap the more options button
|
||||
final moreOptionButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_toolbar_moreOptions.tr(),
|
||||
);
|
||||
await tester.tapButton(moreOptionButton);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton =
|
||||
find.byFlowySvg(FlowySvgs.type_formula_m);
|
||||
final inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
|
||||
// expect to see the math equation block
|
||||
@@ -171,55 +163,5 @@ void main() {
|
||||
lessThan(5),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('insert inline math equation by shortcut', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'insert inline math equation by shortcut',
|
||||
);
|
||||
|
||||
// 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),
|
||||
);
|
||||
|
||||
// mock key event
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyE,
|
||||
isShiftPressed: true,
|
||||
isControlPressed: true,
|
||||
);
|
||||
|
||||
// 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+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);
|
||||
|
||||
-1
@@ -176,7 +176,6 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_outline.tr(),
|
||||
offset: 180,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
+8
-3
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
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';
|
||||
@@ -86,10 +85,16 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_text_format_m));
|
||||
await tester.tapButton(find.byType(HeadingPopup));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(HeadingButton),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
|
||||
// tap the H1 button
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.type_h1_m).at(0));
|
||||
await tester.tapButton(find.byType(HeadingButton).at(0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
+4
-6
@@ -35,12 +35,10 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
)
|
||||
.first,
|
||||
find.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
),
|
||||
onHover: () async {
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
+7
-19
@@ -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,31 +45,24 @@ 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://self-hosted.appflowy.cloud';
|
||||
const serverUrl = 'https://test.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(DesktopToast));
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// open settings page to check the result
|
||||
@@ -86,11 +79,6 @@ void main() {
|
||||
find.text(serverUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
// check the web url
|
||||
expect(
|
||||
find.text(webUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// reset to appflowy cloud
|
||||
await tester.tapButton(
|
||||
@@ -102,7 +90,7 @@ void main() {
|
||||
);
|
||||
|
||||
// wait the app to restart, and the tooltip to disappear
|
||||
await tester.pumpUntilNotFound(find.byType(DesktopToast));
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
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';
|
||||
|
||||
@@ -48,82 +44,5 @@ 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,58 +196,5 @@ 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']);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
+19
-136
@@ -1,11 +1,12 @@
|
||||
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/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.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:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -26,6 +27,21 @@ void main() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
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.content,
|
||||
firstIcon.name,
|
||||
builtInSpaceColors.first,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('Update page emoji in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -144,7 +160,7 @@ void main() {
|
||||
testWidgets('Update page icon in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
final iconData = await loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
@@ -176,7 +192,7 @@ void main() {
|
||||
testWidgets('Update page icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
final iconData = await loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
@@ -210,137 +226,4 @@ void main() {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page custom image icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// prepare local image
|
||||
final iconData = await tester.prepareImageIcon();
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page custom svg icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// prepare local image
|
||||
final iconData = await tester.prepareSvgIcon();
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page custom svg icon in title bar by pasting a link',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// prepare local image
|
||||
const testIconLink =
|
||||
'https://beta.appflowy.cloud/api/file_storage/008e6f23-516b-4d8d-b1fe-2b75c51eee26/v1/blob/6bdf8dff%2D0e54%2D4d35%2D9981%2Dcde68bef1141/BGpLnRtb3AGBNgSJsceu70j83zevYKrMLzqsTIJcBeI=.svg';
|
||||
|
||||
/// 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.updatePageIconInTitleBarByPasteALink(
|
||||
name: value.name,
|
||||
layout: value,
|
||||
iconLink: testIconLink,
|
||||
);
|
||||
|
||||
/// check if there is a svg in page
|
||||
final pageName = tester.findPageName(
|
||||
value.name,
|
||||
layout: value,
|
||||
);
|
||||
final imageInPage = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.byType(SvgPicture),
|
||||
);
|
||||
expect(imageInPage, findsOneWidget);
|
||||
|
||||
/// check if there is a svg in title
|
||||
final imageInTitle = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.byWidgetPredicate((w) {
|
||||
if (w is! SvgPicture) return false;
|
||||
final loader = w.bytesLoader;
|
||||
if (loader is! SvgFileLoader) return false;
|
||||
return loader.file.path.endsWith('.svg');
|
||||
}),
|
||||
);
|
||||
expect(imageInTitle, findsOneWidget);
|
||||
}
|
||||
});
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
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,7 +2,6 @@ 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;
|
||||
|
||||
@@ -15,5 +14,4 @@ void main() {
|
||||
sidebar_favorite_test.main();
|
||||
sidebar_icon_test.main();
|
||||
sidebar_view_item_test.main();
|
||||
sidebar_recent_icon_test.main();
|
||||
}
|
||||
+22
-146
@@ -1,166 +1,42 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/emoji/emoji_handler.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/keyboard.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> prepare(WidgetTester tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
}
|
||||
|
||||
// May be better to move this to an existing test but unsure what it fits with
|
||||
group('Keyboard shortcuts related to emojis', () {
|
||||
testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker',
|
||||
(tester) async {
|
||||
await prepare(tester);
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
expect(find.byType(EmojiHandler), findsNothing);
|
||||
final Finder editor = find.byType(AppFlowyEditor);
|
||||
await tester.tap(editor);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyE,
|
||||
isAltPressed: true,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isControlPressed: !Platform.isMacOS,
|
||||
expect(find.byType(EmojiSelectionMenu), findsNothing);
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.alt,
|
||||
LogicalKeyboardKey.keyE,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
expect(find.byType(EmojiHandler), findsOneWidget);
|
||||
|
||||
/// press backspace to hide the emoji picker
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
expect(find.byType(EmojiHandler), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('insert emoji by slash menu', (tester) async {
|
||||
await prepare(tester);
|
||||
await tester.editor.showSlashMenu();
|
||||
|
||||
/// show emoji picler
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_emoji.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
expect(find.byType(EmojiHandler), findsOneWidget);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
|
||||
/// except the emoji is in document
|
||||
expect(firstNode.delta!.toPlainText().contains('😀'), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('insert emoji by colon', () {
|
||||
Future<void> createNewDocumentAndShowEmojiList(
|
||||
WidgetTester tester, {
|
||||
String? search,
|
||||
}) async {
|
||||
await prepare(tester);
|
||||
await tester.ime.insertText(':${search ?? 'a'}');
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
testWidgets('insert with click', (tester) async {
|
||||
await createNewDocumentAndShowEmojiList(tester);
|
||||
|
||||
/// emoji list is showing
|
||||
final emojiHandler = find.byType(EmojiHandler);
|
||||
expect(emojiHandler, findsOneWidget);
|
||||
final emojiButtons =
|
||||
find.descendant(of: emojiHandler, matching: find.byType(FlowyButton));
|
||||
final firstTextFinder = find.descendant(
|
||||
of: emojiButtons.first,
|
||||
matching: find.byType(FlowyText),
|
||||
);
|
||||
final emojiText =
|
||||
(firstTextFinder.evaluate().first.widget as FlowyText).text;
|
||||
|
||||
/// click first emoji item
|
||||
await tester.tapButton(emojiButtons.first);
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
|
||||
/// except the emoji is in document
|
||||
expect(emojiText.contains(firstNode.delta!.toPlainText()), true);
|
||||
});
|
||||
|
||||
testWidgets('insert with arrow and enter', (tester) async {
|
||||
await createNewDocumentAndShowEmojiList(tester);
|
||||
|
||||
/// emoji list is showing
|
||||
final emojiHandler = find.byType(EmojiHandler);
|
||||
expect(emojiHandler, findsOneWidget);
|
||||
final emojiButtons =
|
||||
find.descendant(of: emojiHandler, matching: find.byType(FlowyButton));
|
||||
|
||||
/// tap arrow down and arrow up
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
|
||||
final firstTextFinder = find.descendant(
|
||||
of: emojiButtons.first,
|
||||
matching: find.byType(FlowyText),
|
||||
);
|
||||
final emojiText =
|
||||
(firstTextFinder.evaluate().first.widget as FlowyText).text;
|
||||
|
||||
/// tap enter
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
|
||||
/// except the emoji is in document
|
||||
expect(emojiText.contains(firstNode.delta!.toPlainText()), true);
|
||||
});
|
||||
|
||||
testWidgets('insert with searching', (tester) async {
|
||||
await createNewDocumentAndShowEmojiList(tester, search: 's');
|
||||
|
||||
/// search for `smiling eyes`, IME is not working, use keyboard input
|
||||
final searchText = [
|
||||
LogicalKeyboardKey.keyM,
|
||||
LogicalKeyboardKey.keyI,
|
||||
LogicalKeyboardKey.keyL,
|
||||
LogicalKeyboardKey.keyI,
|
||||
LogicalKeyboardKey.keyN,
|
||||
LogicalKeyboardKey.keyG,
|
||||
LogicalKeyboardKey.space,
|
||||
LogicalKeyboardKey.keyE,
|
||||
LogicalKeyboardKey.keyY,
|
||||
LogicalKeyboardKey.keyE,
|
||||
LogicalKeyboardKey.keyS,
|
||||
];
|
||||
|
||||
for (final key in searchText) {
|
||||
await tester.simulateKeyEvent(key);
|
||||
}
|
||||
|
||||
/// tap enter
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
|
||||
/// except the emoji is in document
|
||||
expect(firstNode.delta!.toPlainText().contains('😄'), true);
|
||||
});
|
||||
|
||||
testWidgets('start searching with sapce', (tester) async {
|
||||
await createNewDocumentAndShowEmojiList(tester, search: ' ');
|
||||
|
||||
/// emoji list is showing
|
||||
final emojiHandler = find.byType(EmojiHandler);
|
||||
expect(emojiHandler, findsNothing);
|
||||
expect(find.byType(EmojiSelectionMenu), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.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';
|
||||
|
||||
@@ -37,7 +38,7 @@ void main() {
|
||||
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
@@ -47,7 +48,7 @@ void main() {
|
||||
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.dark);
|
||||
@@ -65,11 +66,10 @@ void main() {
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// disable it temporarily. It works on macOS but not on Linux.
|
||||
// themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
// expect(themeMode, ThemeMode.light);
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
});
|
||||
|
||||
testWidgets('show or hide home menu', (tester) async {
|
||||
|
||||
+8
-50
@@ -1,16 +1,12 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
import '../document/document_with_database_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -22,7 +18,7 @@ void main() {
|
||||
|
||||
// mock the file picker
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(context.applicationDataDirectory, 'test.zip'),
|
||||
p.join(context.applicationDataDirectory, 'test.md'),
|
||||
);
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
@@ -32,14 +28,10 @@ void main() {
|
||||
tester.expectToExportSuccess();
|
||||
|
||||
final file = File(path);
|
||||
expect(file.existsSync(), true);
|
||||
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
|
||||
for (final entry in archive) {
|
||||
if (entry.isFile && entry.name.endsWith('.md')) {
|
||||
final markdown = utf8.decode(entry.content);
|
||||
expect(markdown, expectedMarkdown);
|
||||
}
|
||||
}
|
||||
final isExist = file.existsSync();
|
||||
expect(isExist, true);
|
||||
final markdown = file.readAsStringSync();
|
||||
expect(markdown, expectedMarkdown);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
@@ -65,7 +57,7 @@ void main() {
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(
|
||||
context.applicationDataDirectory,
|
||||
'${shareButtonState.view.name}.zip',
|
||||
'${shareButtonState.view.name}.md',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -77,44 +69,10 @@ void main() {
|
||||
tester.expectToExportSuccess();
|
||||
|
||||
final file = File(path);
|
||||
expect(file.existsSync(), true);
|
||||
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
|
||||
for (final entry in archive) {
|
||||
if (entry.isFile && entry.name.endsWith('.md')) {
|
||||
final markdown = utf8.decode(entry.content);
|
||||
expect(markdown, expectedMarkdown);
|
||||
}
|
||||
}
|
||||
final isExist = file.existsSync();
|
||||
expect(isExist, true);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('share the markdown with database', (tester) async {
|
||||
final context = await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
|
||||
|
||||
// mock the file picker
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(context.applicationDataDirectory, 'test.zip'),
|
||||
);
|
||||
// click the share button and select markdown
|
||||
await tester.tapShareButton();
|
||||
await tester.tapMarkdownButton();
|
||||
|
||||
// expect to see the success dialog
|
||||
tester.expectToExportSuccess();
|
||||
|
||||
final file = File(path);
|
||||
expect(file.existsSync(), true);
|
||||
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
|
||||
bool hasCsvFile = false;
|
||||
for (final entry in archive) {
|
||||
if (entry.isFile && entry.name.endsWith('.csv')) {
|
||||
hasCsvFile = true;
|
||||
}
|
||||
}
|
||||
expect(hasCsvFile, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
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';
|
||||
@@ -289,39 +285,6 @@ void main() {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ void main() {
|
||||
hotkeys_test.main();
|
||||
emoji_shortcut_test.main();
|
||||
hotkeys_test.main();
|
||||
emoji_shortcut_test.main();
|
||||
share_markdown_test.main();
|
||||
import_files_test.main();
|
||||
zoom_in_out_test.main();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'desktop/database/database_icon_test.dart' as database_icon_test;
|
||||
import 'desktop/first_test/first_test.dart' as first_test;
|
||||
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
|
||||
import 'desktop/uncategorized/code_block_language_selector_test.dart'
|
||||
as code_language_selector;
|
||||
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
|
||||
import 'desktop/first_test/first_test.dart' as first_test;
|
||||
|
||||
Future<void> main() async {
|
||||
await runIntegration9OnDesktop();
|
||||
@@ -16,5 +15,4 @@ Future<void> runIntegration9OnDesktop() async {
|
||||
first_test.main();
|
||||
tabs_test.main();
|
||||
code_language_selector.main();
|
||||
database_icon_test.main();
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
|
||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const title = 'Test At Menu';
|
||||
|
||||
group('at menu', () {
|
||||
testWidgets('show at menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowAtMenu(title);
|
||||
final menuWidget = find.byType(MobileInlineActionsMenu);
|
||||
expect(menuWidget, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('search by at menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowAtMenu(title);
|
||||
const searchText = gettingStarted;
|
||||
await tester.ime.insertText(searchText);
|
||||
final actionWidgets = find.byType(MobileInlineActionsWidget);
|
||||
expect(actionWidgets, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('tap at menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowAtMenu(title);
|
||||
const searchText = gettingStarted;
|
||||
await tester.ime.insertText(searchText);
|
||||
final actionWidgets = find.byType(MobileInlineActionsWidget);
|
||||
await tester.tap(actionWidgets.last);
|
||||
expect(find.byType(MentionPageBlock), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('create subpage with at menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile(title);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
const subpageName = 'Subpage';
|
||||
await tester.ime.insertText('[[$subpageName');
|
||||
await tester.pumpAndSettle();
|
||||
final actionWidgets = find.byType(MobileInlineActionsWidget);
|
||||
await tester.tapButton(actionWidgets.first);
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0]);
|
||||
assert(firstNode != null);
|
||||
expect(firstNode!.delta?.toPlainText().contains('['), false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'at_menu_test.dart' as at_menu;
|
||||
import 'at_menu_test.dart' as at_menu_test;
|
||||
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 'slash_menu_test.dart' as slash_menu;
|
||||
import 'title_test.dart' as title_test;
|
||||
import 'toolbar_test.dart' as toolbar_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -16,9 +12,5 @@ void main() {
|
||||
title_test.main();
|
||||
page_style_test.main();
|
||||
plus_menu_test.main();
|
||||
at_menu_test.main();
|
||||
simple_table_test.main();
|
||||
toolbar_test.main();
|
||||
slash_menu.main();
|
||||
at_menu.main();
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.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();
|
||||
|
||||
group('document title:', () {
|
||||
testWidgets('update page custom image icon in title bar', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
/// prepare local image
|
||||
final iconData = await tester.prepareImageIcon();
|
||||
|
||||
/// create an empty page
|
||||
await tester
|
||||
.tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));
|
||||
|
||||
/// show Page style page
|
||||
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
|
||||
final pageStyleIcon = find.byType(PageStyleIcon);
|
||||
final iconInPageStyleIcon = find.descendant(
|
||||
of: pageStyleIcon,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
expect(iconInPageStyleIcon, findsNothing);
|
||||
|
||||
/// show icon picker
|
||||
await tester.tapButton(pageStyleIcon);
|
||||
|
||||
/// upload custom icon
|
||||
await tester.pickImage(iconData);
|
||||
|
||||
/// check result
|
||||
final documentPage = find.byType(MobileDocumentScreen);
|
||||
final rawEmojiIconFinder = find
|
||||
.descendant(
|
||||
of: documentPage,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
)
|
||||
.last;
|
||||
final rawEmojiIconWidget =
|
||||
rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final iconDataInWidget = rawEmojiIconWidget.emoji;
|
||||
expect(iconDataInWidget.type, FlowyIconType.custom);
|
||||
final imageFinder =
|
||||
find.descendant(of: rawEmojiIconFinder, matching: find.byType(Image));
|
||||
expect(imageFinder, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('update page custom svg icon in title bar', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
/// prepare local image
|
||||
final iconData = await tester.prepareSvgIcon();
|
||||
|
||||
/// create an empty page
|
||||
await tester
|
||||
.tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));
|
||||
|
||||
/// show Page style page
|
||||
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
|
||||
final pageStyleIcon = find.byType(PageStyleIcon);
|
||||
final iconInPageStyleIcon = find.descendant(
|
||||
of: pageStyleIcon,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
expect(iconInPageStyleIcon, findsNothing);
|
||||
|
||||
/// show icon picker
|
||||
await tester.tapButton(pageStyleIcon);
|
||||
|
||||
/// upload custom icon
|
||||
await tester.pickImage(iconData);
|
||||
|
||||
/// check result
|
||||
final documentPage = find.byType(MobileDocumentScreen);
|
||||
final rawEmojiIconFinder = find
|
||||
.descendant(
|
||||
of: documentPage,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
)
|
||||
.last;
|
||||
final rawEmojiIconWidget =
|
||||
rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final iconDataInWidget = rawEmojiIconWidget.emoji;
|
||||
expect(iconDataInWidget.type, FlowyIconType.custom);
|
||||
final svgFinder = find.descendant(
|
||||
of: rawEmojiIconFinder,
|
||||
matching: find.byType(SvgPicture),
|
||||
);
|
||||
expect(svgFinder, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,30 +1,17 @@
|
||||
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() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document page style:', () {
|
||||
double getCurrentEditorFontSize() {
|
||||
@@ -127,37 +114,5 @@ 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
|
||||
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.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';
|
||||
@@ -88,32 +85,5 @@ void main() {
|
||||
equals(Selection.collapsed(Position(path: [2]))),
|
||||
);
|
||||
});
|
||||
|
||||
const title = 'Test Plus Menu';
|
||||
testWidgets('show plus menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowPlusMenu(title);
|
||||
final menuWidget = find.byType(MobileInlineActionsMenu);
|
||||
expect(menuWidget, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('search by plus menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowPlusMenu(title);
|
||||
const searchText = gettingStarted;
|
||||
await tester.ime.insertText(searchText);
|
||||
final actionWidgets = find.byType(MobileInlineActionsWidget);
|
||||
expect(actionWidgets, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('tap plus menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowPlusMenu(title);
|
||||
const searchText = gettingStarted;
|
||||
await tester.ime.insertText(searchText);
|
||||
final actionWidgets = find.byType(MobileInlineActionsWidget);
|
||||
await tester.tap(actionWidgets.last);
|
||||
expect(find.byType(MentionPageBlock), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,10 +2,8 @@ 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';
|
||||
|
||||
@@ -245,53 +243,6 @@ void main() {
|
||||
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])!;
|
||||
@@ -420,22 +371,13 @@ void main() {
|
||||
// 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.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
clearContents,
|
||||
100,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await tester.tapButton(clearContents);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the first cell is empty
|
||||
@@ -485,7 +427,7 @@ void main() {
|
||||
// open the plus menu and select the heading block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.editor_heading1.tr(),
|
||||
LocaleKeys.editor_toggleHeading1ShortForm.tr(),
|
||||
);
|
||||
|
||||
// check the heading block is inserted
|
||||
@@ -494,61 +436,5 @@ void main() {
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.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();
|
||||
|
||||
const title = 'Test Slash Menu';
|
||||
|
||||
group('slash menu', () {
|
||||
testWidgets('show slash menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowSlashMenu(title);
|
||||
final menuWidget = find.byType(MobileSelectionMenuWidget);
|
||||
expect(menuWidget, findsOneWidget);
|
||||
final items =
|
||||
(menuWidget.evaluate().first.widget as MobileSelectionMenuWidget)
|
||||
.items;
|
||||
int i = 0;
|
||||
for (final item in items) {
|
||||
final localItem = mobileItems[i];
|
||||
expect(item.name, localItem.name);
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('search by slash menu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createPageAndShowSlashMenu(title);
|
||||
const searchText = 'Heading';
|
||||
await tester.ime.insertText(searchText);
|
||||
final itemWidgets = find.byType(MobileSelectionMenuItemWidget);
|
||||
int number = 0;
|
||||
for (final item in mobileItems) {
|
||||
if (item is MobileSelectionMenuItem) {
|
||||
for (final childItem in item.children) {
|
||||
if (childItem.name
|
||||
.toLowerCase()
|
||||
.contains(searchText.toLowerCase())) {
|
||||
number++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item.name.toLowerCase().contains(searchText.toLowerCase())) {
|
||||
number++;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(itemWidgets, findsNWidgets(number));
|
||||
});
|
||||
|
||||
testWidgets('tap to show submenu', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile(title);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
final listview = find.descendant(
|
||||
of: find.byType(MobileSelectionMenuWidget),
|
||||
matching: find.byType(ListView),
|
||||
);
|
||||
for (final item in mobileItems) {
|
||||
if (item is! MobileSelectionMenuItem) continue;
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text(item.name),
|
||||
50,
|
||||
scrollable: listview,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
);
|
||||
await tester.tap(find.text(item.name));
|
||||
final childrenLength = ((listview.evaluate().first.widget as ListView)
|
||||
.childrenDelegate as SliverChildListDelegate)
|
||||
.children
|
||||
.length;
|
||||
expect(childrenLength, item.children.length);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.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/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> createNeaPage(WidgetTester tester) async {
|
||||
final createPageButton =
|
||||
find.byKey(BottomNavigationBarItemType.add.valueKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
final editor = find.byType(AppFlowyEditor);
|
||||
expect(editor, findsOneWidget);
|
||||
}
|
||||
|
||||
const testLink = 'https://appflowy.io/';
|
||||
|
||||
group('links', () {
|
||||
testWidgets('insert links', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
await createNeaPage(tester);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
/// insert two lines of text
|
||||
const strFirst = 'FirstLine', strSecond = 'SecondLine';
|
||||
await tester.ime.insertText(strFirst);
|
||||
await editorState.insertNewLine();
|
||||
await tester.ime.insertText(strSecond);
|
||||
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.doubleTapAt(tester.getCenter(firstLine));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// find link button and tap it
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// input the link
|
||||
final textFormField = find.byType(TextFormField);
|
||||
expect(textFormField, findsNWidgets(2));
|
||||
final linkField = textFormField.last;
|
||||
await tester.enterText(linkField, testLink);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));
|
||||
|
||||
/// apply the link
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
/// do it again
|
||||
/// select the second line
|
||||
await tester.doubleTapAt(tester.getCenter(secondLine));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(linkButton);
|
||||
await tester.enterText(linkField, testLink);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));
|
||||
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': testLink, 'is_page_link': false},
|
||||
};
|
||||
|
||||
expect(
|
||||
firstNode?.delta?.toJson(),
|
||||
[commonDeltaJson(strFirst)],
|
||||
);
|
||||
expect(
|
||||
secondNode?.delta?.toJson(),
|
||||
[commonDeltaJson(strSecond)],
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('change a link', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
await createNeaPage(tester);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
const testText = 'TestText';
|
||||
await tester.ime.insertText(testText);
|
||||
final textFinder = find.text(testText, findRichText: true);
|
||||
|
||||
/// select the first line
|
||||
await tester.doubleTapAt(tester.getCenter(textFinder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// find link button and tap it
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// input the link
|
||||
final textFormField = find.byType(TextFormField);
|
||||
expect(textFormField, findsNWidgets(2));
|
||||
final linkField = textFormField.last;
|
||||
await tester.enterText(linkField, testLink);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));
|
||||
|
||||
/// apply the link
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
/// show edit link menu
|
||||
await tester.longPress(textFinder);
|
||||
await tester.pumpAndSettle();
|
||||
final linkEditMenu = find.byType(MobileBottomSheetEditLinkWidget);
|
||||
expect(linkEditMenu, findsOneWidget);
|
||||
|
||||
/// remove the link
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toJson(), [{'insert': testText}]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_ask_ai_entrance.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_result.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_textfield.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/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Search test', () {
|
||||
testWidgets('tap to search page', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
final searchButton = find.byFlowySvg(FlowySvgs.m_home_search_icon_m);
|
||||
await tester.tapButton(searchButton);
|
||||
|
||||
///check for UI element
|
||||
expect(find.byType(MobileSearchAskAiEntrance), findsNothing);
|
||||
expect(find.byType(MobileSearchRecentList), findsOneWidget);
|
||||
expect(find.byType(MobileSearchResultList), findsNothing);
|
||||
|
||||
/// search for something
|
||||
final searchTextField = find.descendant(
|
||||
of: find.byType(MobileSearchTextfield),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
final query = '$gettingStarted searching';
|
||||
await tester.enterText(searchTextField, query);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MobileSearchRecentList), findsNothing);
|
||||
expect(find.byType(MobileSearchResultList), findsOneWidget);
|
||||
expect(
|
||||
find.text(LocaleKeys.search_noResultForSearching.tr(args: [query])),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
/// clear text
|
||||
final clearButton = find.byFlowySvg(FlowySvgs.clear_s);
|
||||
await tester.tapButton(clearButton);
|
||||
expect(find.byType(MobileSearchRecentList), findsOneWidget);
|
||||
expect(find.byType(MobileSearchResultList), findsNothing);
|
||||
|
||||
/// tap cancel button
|
||||
final cancelButton = find.text(LocaleKeys.button_cancel.tr());
|
||||
expect(cancelButton, findsNothing);
|
||||
await tester.enterText(searchTextField, query);
|
||||
await tester.pumpAndSettle();
|
||||
expect(cancelButton, findsOneWidget);
|
||||
await tester.tapButton(cancelButton);
|
||||
expect(cancelButton, findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.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/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('test for change scale factor', (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 [Font Scale Factor]
|
||||
await tester.tapButton(
|
||||
find.text(LocaleKeys.settings_appearance_fontScaleFactor.tr()),
|
||||
);
|
||||
|
||||
/// drag slider
|
||||
final slider = find.descendant(
|
||||
of: find.byType(FontSizeStepper),
|
||||
matching: find.byType(Slider),
|
||||
);
|
||||
await tester.slideToValue(slider, 0.8);
|
||||
expect(appflowyScaleFactor, 0.8);
|
||||
|
||||
await tester.slideToValue(slider, 0.9);
|
||||
expect(appflowyScaleFactor, 0.9);
|
||||
|
||||
await tester.slideToValue(slider, 1.0);
|
||||
expect(appflowyScaleFactor, 1.0);
|
||||
|
||||
await tester.slideToValue(slider, 1.1);
|
||||
expect(appflowyScaleFactor, 1.1);
|
||||
|
||||
await tester.slideToValue(slider, 1.2);
|
||||
expect(appflowyScaleFactor, 1.2);
|
||||
});
|
||||
}
|
||||
@@ -3,8 +3,6 @@ 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 {
|
||||
@@ -19,5 +17,4 @@ Future<void> runIntegration1OnMobile() async {
|
||||
anonymous_sign_in_test.main();
|
||||
create_new_page_test.main();
|
||||
document_test_runner.main();
|
||||
default_text_direction_test.main();
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -176,33 +175,6 @@ 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,
|
||||
@@ -236,25 +208,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
Future<void> wait(int milliseconds) async {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> slideToValue(
|
||||
Finder slider,
|
||||
double value, {
|
||||
double paddingOffset = 24.0,
|
||||
}) async {
|
||||
final sliderWidget = slider.evaluate().first.widget as Slider;
|
||||
final range = sliderWidget.max - sliderWidget.min;
|
||||
final initialRate = (value - sliderWidget.min) / range;
|
||||
final totalWidth = getSize(slider).width - (2 * paddingOffset);
|
||||
final zeroPoint = getTopLeft(slider) +
|
||||
Offset(
|
||||
paddingOffset + initialRate * totalWidth,
|
||||
getSize(slider).height / 2,
|
||||
);
|
||||
final calculatedOffset = value * (totalWidth / 100);
|
||||
await dragFrom(zeroPoint, Offset(calculatedOffset, 0));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
extension AppFlowyFinderTestBase on CommonFinders {
|
||||
|
||||
@@ -13,19 +13,16 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
|
||||
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';
|
||||
@@ -50,8 +47,6 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'emoji.dart';
|
||||
@@ -67,10 +62,12 @@ extension CommonOperations on WidgetTester {
|
||||
} else {
|
||||
// cloud version
|
||||
final anonymousButton = find.byType(SignInAnonymousButtonV2);
|
||||
await tapButton(anonymousButton, warnIfMissed: true);
|
||||
await tapButton(anonymousButton);
|
||||
}
|
||||
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
if (Platform.isWindows) {
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapContinousAnotherWay() async {
|
||||
@@ -601,23 +598,6 @@ 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(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
// tap the button with [FlowySvgData]
|
||||
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
@@ -629,7 +609,7 @@ extension CommonOperations on WidgetTester {
|
||||
// update the page icon in the sidebar
|
||||
Future<void> updatePageIconInSidebarByName({
|
||||
required String name,
|
||||
String? parentName,
|
||||
required String parentName,
|
||||
required ViewLayoutPB layout,
|
||||
required EmojiIconData icon,
|
||||
}) async {
|
||||
@@ -671,31 +651,10 @@ extension CommonOperations on WidgetTester {
|
||||
await tapEmoji(icon.emoji);
|
||||
} else if (icon.type == FlowyIconType.icon) {
|
||||
await tapIcon(icon);
|
||||
} else if (icon.type == FlowyIconType.custom) {
|
||||
await pickImage(icon);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> updatePageIconInTitleBarByPasteALink({
|
||||
required String name,
|
||||
required ViewLayoutPB layout,
|
||||
required String iconLink,
|
||||
}) async {
|
||||
await openPage(
|
||||
name,
|
||||
layout: layout,
|
||||
);
|
||||
final title = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(name),
|
||||
);
|
||||
await tapButton(title);
|
||||
await tapButton(find.byType(EmojiPickerButton));
|
||||
await pasteImageLinkAsIcon(iconLink);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> openNotificationHub({int tabIndex = 0}) async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(NotificationButton),
|
||||
@@ -939,60 +898,6 @@ extension CommonOperations on WidgetTester {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<EmojiIconData> prepareImageIcon() async {
|
||||
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());
|
||||
return EmojiIconData.custom(imageFile.path);
|
||||
}
|
||||
|
||||
Future<EmojiIconData> prepareSvgIcon() async {
|
||||
final imagePath = await rootBundle.load('assets/test/images/sample.svg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final localImagePath = p.join(tempDirectory.path, 'sample.svg');
|
||||
final imageFile = File(localImagePath)
|
||||
..writeAsBytesSync(imagePath.buffer.asUint8List());
|
||||
return EmojiIconData.custom(imageFile.path);
|
||||
}
|
||||
|
||||
/// create new page and show slash menu
|
||||
Future<void> createPageAndShowSlashMenu(String title) async {
|
||||
await createNewDocumentOnMobile(title);
|
||||
await editor.tapLineOfEditorAt(0);
|
||||
await editor.showSlashMenu();
|
||||
}
|
||||
|
||||
/// create new page and show at menu
|
||||
Future<void> createPageAndShowAtMenu(String title) async {
|
||||
await createNewDocumentOnMobile(title);
|
||||
await editor.tapLineOfEditorAt(0);
|
||||
await editor.showAtMenu();
|
||||
}
|
||||
|
||||
/// create new page and show plus menu
|
||||
Future<void> createPageAndShowPlusMenu(String title) async {
|
||||
await createNewDocumentOnMobile(title);
|
||||
await editor.tapLineOfEditorAt(0);
|
||||
await editor.showPlusMenu();
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsFinder on CommonFinders {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.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:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
|
||||
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
|
||||
@@ -19,11 +27,10 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
|
||||
@@ -37,7 +44,6 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filt
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
|
||||
@@ -70,8 +76,6 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio
|
||||
import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.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/util/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
|
||||
@@ -86,9 +90,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
// Non-exported member of the table_calendar library
|
||||
@@ -942,31 +943,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Future<void> changeFieldWidth(String fieldName, double width) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
|
||||
);
|
||||
await hoverOnWidget(
|
||||
field,
|
||||
onHover: () async {
|
||||
final dragHandle = find.descendant(
|
||||
of: field,
|
||||
matching: find.byType(DragToExpandLine),
|
||||
);
|
||||
await drag(dragHandle, Offset(width - getSize(field).width, 0));
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double getFieldWidth(String fieldName) {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
|
||||
);
|
||||
|
||||
return getSize(field).width;
|
||||
}
|
||||
|
||||
Future<void> findDateEditor(dynamic matcher) async {
|
||||
final finder = find.byType(DateCellEditor);
|
||||
expect(finder, matcher);
|
||||
@@ -1488,7 +1464,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
await tapButtonWithName(LocaleKeys.button_delete.tr());
|
||||
}
|
||||
|
||||
Future<void> dragDropRescheduleCalendarEvent() async {
|
||||
@@ -1596,7 +1571,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
of: textField,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowySvg && widget.svg == FlowySvgs.close_filled_s,
|
||||
widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -307,11 +307,9 @@ class EditorOperations {
|
||||
Future<void> openTurnIntoMenu(Path path) async {
|
||||
await hoverAndClickOptionMenuButton(path);
|
||||
await tester.tapButton(
|
||||
find
|
||||
.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
|
||||
)
|
||||
.first,
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpUntilFound(find.byType(TurnIntoOptionMenu));
|
||||
}
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
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/icon_uploader.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart';
|
||||
import 'package:flowy_svg/flowy_svg.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 'base.dart';
|
||||
import 'common_operations.dart';
|
||||
|
||||
extension EmojiTestExtension on WidgetTester {
|
||||
Future<void> tapEmoji(String emoji) async {
|
||||
@@ -29,7 +21,7 @@ extension EmojiTestExtension on WidgetTester {
|
||||
await tapButton(emojiWidget);
|
||||
}
|
||||
|
||||
Future<void> tapIcon(EmojiIconData icon, {bool enableColor = true}) async {
|
||||
Future<void> tapIcon(EmojiIconData icon) async {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
|
||||
final pickTab = find.byType(PickerTab);
|
||||
expect(pickTab, findsOneWidget);
|
||||
@@ -39,106 +31,35 @@ extension EmojiTestExtension on WidgetTester {
|
||||
matching: find.text(PickerTabType.icon.tr),
|
||||
);
|
||||
expect(iconTab, findsOneWidget);
|
||||
await tapButton(iconTab);
|
||||
expect(find.byType(FlowyIconPicker), findsNothing);
|
||||
await tap(iconTab);
|
||||
await pumpAndSettle();
|
||||
expect(find.byType(FlowyIconPicker), findsOneWidget);
|
||||
final selectedSvg = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(selectedSvg.first);
|
||||
if (enableColor) {
|
||||
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;
|
||||
}
|
||||
expect(find.byType(IconColorPicker), findsNothing);
|
||||
await tapButton(selectedSvg);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickImage(EmojiIconData icon) async {
|
||||
final pickTab = find.byType(PickerTab);
|
||||
expect(pickTab, findsOneWidget);
|
||||
await pumpAndSettle();
|
||||
|
||||
/// switch to custom tab
|
||||
final iconTab = find.descendant(
|
||||
of: pickTab,
|
||||
matching: find.text(PickerTabType.custom.tr),
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
expect(iconTab, findsOneWidget);
|
||||
await tapButton(iconTab);
|
||||
|
||||
/// mock for dragging image
|
||||
final dropTarget = find.descendant(
|
||||
of: find.byType(IconUploader),
|
||||
matching: find.byType(DropTarget),
|
||||
);
|
||||
expect(dropTarget, findsOneWidget);
|
||||
final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget;
|
||||
dropTargetWidget.onDragDone?.call(
|
||||
DropDoneDetails(
|
||||
files: [DropItemFile(icon.emoji)],
|
||||
localPosition: Offset.zero,
|
||||
globalPosition: Offset.zero,
|
||||
),
|
||||
);
|
||||
await pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
/// confirm to upload
|
||||
final confirmButton = find.descendant(
|
||||
of: find.byType(IconUploader),
|
||||
matching: find.byType(PrimaryRoundedButton),
|
||||
);
|
||||
await tapButton(confirmButton);
|
||||
}
|
||||
|
||||
Future<void> pasteImageLinkAsIcon(String link) async {
|
||||
final pickTab = find.byType(PickerTab);
|
||||
expect(pickTab, findsOneWidget);
|
||||
await pumpAndSettle();
|
||||
|
||||
/// switch to custom tab
|
||||
final iconTab = find.descendant(
|
||||
of: pickTab,
|
||||
matching: find.text(PickerTabType.custom.tr),
|
||||
);
|
||||
expect(iconTab, findsOneWidget);
|
||||
await tapButton(iconTab);
|
||||
|
||||
// mock the clipboard
|
||||
await getIt<ClipboardService>()
|
||||
.setData(ClipboardServiceData(plainText: link));
|
||||
|
||||
// paste the link
|
||||
await simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await pumpAndSettle(const Duration(seconds: 5));
|
||||
|
||||
/// confirm to upload
|
||||
final confirmButton = find.descendant(
|
||||
of: find.byType(IconUploader),
|
||||
matching: find.byType(PrimaryRoundedButton),
|
||||
);
|
||||
await tapButton(confirmButton);
|
||||
await tapButton(selectedColor);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ 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/appflowy_network_image.dart';
|
||||
import 'package:appflowy/shared/appflowy_network_svg.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';
|
||||
@@ -23,7 +21,6 @@ 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:string_validator/string_validator.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@@ -248,27 +245,10 @@ extension Expectation on WidgetTester {
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.custom) {
|
||||
final isSvg = data.emoji.endsWith('.svg');
|
||||
if (isURL(data.emoji)) {
|
||||
final image = find.descendant(
|
||||
of: pageName,
|
||||
matching: isSvg
|
||||
? find.byType(FlowyNetworkSvg)
|
||||
: find.byType(FlowyNetworkImage),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
} else {
|
||||
final image = find.descendant(
|
||||
of: pageName,
|
||||
matching: isSvg ? find.byType(SvgPicture) : find.byType(Image),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,34 +269,10 @@ extension Expectation on WidgetTester {
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.custom) {
|
||||
final isSvg = data.emoji.endsWith('.svg');
|
||||
if (isURL(data.emoji)) {
|
||||
final image = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: isSvg
|
||||
? find.byType(FlowyNetworkSvg)
|
||||
: find.byType(FlowyNetworkImage),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
} else {
|
||||
final image = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: isSvg
|
||||
? find.byWidgetPredicate((w) {
|
||||
if (w is! SvgPicture) return false;
|
||||
final loader = w.bytesLoader;
|
||||
if (loader is! SvgFileLoader) return false;
|
||||
return loader.file.path.endsWith('.svg');
|
||||
})
|
||||
: find.byType(Image),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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:http/http.dart' as http;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MyMockClient extends Mock implements http.Client {
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
final requestType = request.method;
|
||||
final requestUri = request.url;
|
||||
|
||||
if (requestType == 'POST' &&
|
||||
requestUri == OpenAIRequestType.textCompletion.uri) {
|
||||
final responseHeaders = <String, String>{
|
||||
'content-type': 'text/event-stream',
|
||||
};
|
||||
final responseBody = Stream.fromIterable([
|
||||
utf8.encode(
|
||||
'{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
|
||||
),
|
||||
utf8.encode('\n'),
|
||||
utf8.encode('[DONE]'),
|
||||
]);
|
||||
|
||||
// Return a mocked response with the expected data
|
||||
return http.StreamedResponse(responseBody, 200, headers: responseHeaders);
|
||||
}
|
||||
|
||||
// Return an error response for any other request
|
||||
return http.StreamedResponse(const Stream.empty(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
class MockOpenAIRepository extends HttpOpenAIRepository {
|
||||
MockOpenAIRepository() : super(apiKey: 'dummyKey', client: MyMockClient());
|
||||
|
||||
@override
|
||||
Future<void> getStreamedCompletions({
|
||||
required String prompt,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(TextCompletionResponse response) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
bool useAction = false,
|
||||
}) async {
|
||||
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
|
||||
final response = await client.send(request);
|
||||
|
||||
String previousSyntax = '';
|
||||
if (response.statusCode == 200) {
|
||||
await for (final chunk in response.stream
|
||||
.transform(const Utf8Decoder())
|
||||
.transform(const LineSplitter())) {
|
||||
await onStart();
|
||||
final data = chunk.trim().split('data: ');
|
||||
if (data[0] != '[DONE]') {
|
||||
final response = TextCompletionResponse.fromJson(
|
||||
json.decode(data[0]),
|
||||
);
|
||||
if (response.choices.isNotEmpty) {
|
||||
final text = response.choices.first.text;
|
||||
if (text == previousSyntax && text == '\n') {
|
||||
continue;
|
||||
}
|
||||
await onProcess(response);
|
||||
previousSyntax = response.choices.first.text;
|
||||
}
|
||||
} else {
|
||||
await onEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ extension AppFlowySettings on WidgetTester {
|
||||
// Enable editing username
|
||||
final editUsernameFinder = find.descendant(
|
||||
of: find.byType(AccountUserProfile),
|
||||
matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m),
|
||||
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||
);
|
||||
await tap(editUsernameFinder, warnIfMissed: false);
|
||||
await pumpAndSettle();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PODS:
|
||||
- app_links (0.0.2):
|
||||
- app_links (0.0.1):
|
||||
- Flutter
|
||||
- appflowy_backend (0.0.1):
|
||||
- Flutter
|
||||
@@ -66,8 +66,6 @@ PODS:
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- saver_gallery (0.0.1):
|
||||
- Flutter
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
- SDWebImage/Core (5.14.2)
|
||||
@@ -81,7 +79,7 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- super_native_extensions (0.0.1):
|
||||
@@ -92,7 +90,6 @@ PODS:
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
@@ -111,14 +108,13 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- 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/darwin`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -163,56 +159,53 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
saver_gallery:
|
||||
:path: ".symlinks/plugins/saver_gallery/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
super_native_extensions:
|
||||
: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/darwin"
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
|
||||
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
|
||||
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
|
||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
|
||||
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
|
||||
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
COCOAPODS: 1.15.2
|
||||
@@ -1,7 +1,7 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@main
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
|
||||
@@ -1,78 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>AppFlowy</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>appflowy-flutter</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<false />
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>AppFlowy</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>appflowy-flutter</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>AppFlowy needs access to your photos to let you add images to your documents</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>AppFlowy needs access to your photos to let you add images to your photo library</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>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true />
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false />
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<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>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,20 +0,0 @@
|
||||
export 'service/ai_entities.dart';
|
||||
export 'service/ai_prompt_input_bloc.dart';
|
||||
export 'service/ai_prompt_selector_cubit.dart';
|
||||
export 'service/appflowy_ai_service.dart';
|
||||
export 'service/error.dart';
|
||||
export 'service/ai_model_state_notifier.dart';
|
||||
export 'service/select_model_bloc.dart';
|
||||
export 'widgets/loading_indicator.dart';
|
||||
export 'widgets/prompt_input/action_buttons.dart';
|
||||
export 'widgets/prompt_input/desktop_prompt_input.dart';
|
||||
export 'widgets/prompt_input/file_attachment_list.dart';
|
||||
export 'widgets/prompt_input/layout_define.dart';
|
||||
export 'widgets/prompt_input/mention_page_bottom_sheet.dart';
|
||||
export 'widgets/prompt_input/mention_page_menu.dart';
|
||||
export 'widgets/prompt_input/prompt_input_text_controller.dart';
|
||||
export 'widgets/prompt_input/predefined_format_buttons.dart';
|
||||
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
|
||||
export 'widgets/prompt_input/select_sources_menu.dart';
|
||||
export 'widgets/prompt_input/select_model_menu.dart';
|
||||
export 'widgets/prompt_input/send_button.dart';
|
||||
Loaded 100 of 4168 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user