diff --git a/.github/workflows/android_ci.yaml b/.github/workflows/android_ci.yaml new file mode 100644 index 0000000000..b65ceec608 --- /dev/null +++ b/.github/workflows/android_ci.yaml @@ -0,0 +1,126 @@ +# name: Android CI + +# on: +# push: +# branches: +# - "main" +# paths: +# - ".github/workflows/mobile_ci.yaml" +# - "frontend/**" +# - "!frontend/appflowy_tauri/**" + +# pull_request: +# branches: +# - "main" +# paths: +# - ".github/workflows/mobile_ci.yaml" +# - "frontend/**" +# - "!frontend/appflowy_tauri/**" + +# env: +# CARGO_TERM_COLOR: always +# FLUTTER_VERSION: "3.19.0" +# RUST_TOOLCHAIN: "1.75" +# CARGO_MAKE_VERSION: "0.36.6" + +# concurrency: +# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} +# cancel-in-progress: true + +# jobs: +# build: +# if: github.event.pull_request.draft != true +# strategy: +# fail-fast: true +# matrix: +# os: [macos-14] +# runs-on: ${{ matrix.os }} + +# steps: +# - name: Check storage space +# run: df -h + +# # the following step is required to avoid running out of space +# - name: Maximize build space +# if: matrix.os == 'ubuntu-latest' +# run: | +# sudo rm -rf /usr/share/dotnet +# sudo rm -rf /opt/ghc +# sudo rm -rf "/usr/local/share/boost" +# sudo rm -rf "$AGENT_TOOLSDIRECTORY" +# sudo docker image prune --all --force +# sudo rm -rf /opt/hostedtoolcache/codeQL +# sudo rm -rf ${GITHUB_WORKSPACE}/.git +# sudo rm -rf $ANDROID_HOME/ndk + +# - name: Check storage space +# run: df -h + +# - name: Checkout source code +# uses: actions/checkout@v4 + +# - uses: actions/setup-java@v4 +# with: +# distribution: temurin +# java-version: 11 + +# - name: Install Rust toolchain +# id: rust_toolchain +# uses: actions-rs/toolchain@v1 +# with: +# toolchain: ${{ env.RUST_TOOLCHAIN }} +# override: true +# profile: minimal + +# - name: Install flutter +# id: flutter +# uses: subosito/flutter-action@v2 +# with: +# channel: "stable" +# flutter-version: ${{ env.FLUTTER_VERSION }} + +# - uses: gradle/gradle-build-action@v3 +# with: +# gradle-version: 7.4.2 + +# - uses: davidB/rust-cargo-make@v1 +# with: +# version: "0.36.6" + +# - name: Install prerequisites +# working-directory: frontend +# run: | +# rustup target install aarch64-linux-android +# rustup target install x86_64-linux-android +# cargo install --force duckscript_cli +# cargo install cargo-ndk +# if [ "$RUNNER_OS" == "Linux" ]; then +# sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub +# sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list +# sudo apt-get update +# sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev +# sudo apt-get install keybinder-3.0 libnotify-dev +# sudo apt-get install gcc-multilib +# elif [ "$RUNNER_OS" == "Windows" ]; then +# vcpkg integrate install +# elif [ "$RUNNER_OS" == "macOS" ]; then +# echo 'do nothing' +# fi +# cargo make appflowy-flutter-deps-tools +# shell: bash + +# - name: Build AppFlowy +# working-directory: frontend +# run: | +# cargo make --profile development-android appflowy-android-dev-ci + + +# - name: Run integration tests +# # https://github.com/ReactiveCircus/android-emulator-runner +# uses: reactivecircus/android-emulator-runner@v2 +# with: +# api-level: 32 +# arch: arm64-v8a +# disk-size: 2048M +# working-directory: frontend/appflowy_flutter +# script: flutter test integration_test/runner.dart \ No newline at end of file diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml new file mode 100644 index 0000000000..e679841a1b --- /dev/null +++ b/.github/workflows/ios_ci.yaml @@ -0,0 +1,91 @@ +name: iOS CI + +on: + push: + branches: + - "main" + paths: + - ".github/workflows/mobile_ci.yaml" + - "frontend/**" + - "!frontend/appflowy_tauri/**" + + pull_request: + branches: + - "main" + paths: + - ".github/workflows/mobile_ci.yaml" + - "frontend/**" + - "!frontend/appflowy_tauri/**" + +env: + FLUTTER_VERSION: "3.19.0" + RUST_TOOLCHAIN: "1.75" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: true + matrix: + os: [macos-14] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + target: aarch64-apple-ios-sim + override: true + profile: minimal + + - name: Install flutter + id: flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.os }} + workspaces: | + frontend/rust-lib + + - uses: davidB/rust-cargo-make@v1 + with: + version: "0.36.6" + + - name: Install prerequisites + working-directory: frontend + run: | + rustup target install aarch64-apple-ios-sim + cargo install --force duckscript_cli + cargo install cargo-lipo + cargo make appflowy-flutter-deps-tools + shell: bash + + - name: Build AppFlowy + working-directory: frontend + run: | + cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios + cargo make --profile development-ios-arm64-sim code_generation + + - uses: futureware-tech/simulator-action@v3 + id: simulator-action + with: + model: 'iPhone 15' + shutdown_after_job: false + + - name: Run integration tests + working-directory: frontend/appflowy_flutter + run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }} diff --git a/.github/workflows/mobile_ci.yaml b/.github/workflows/mobile_ci.yaml deleted file mode 100644 index edb5b93f1e..0000000000 --- a/.github/workflows/mobile_ci.yaml +++ /dev/null @@ -1,117 +0,0 @@ -name: Mobile-CI - -on: - push: - branches: - - "main" - paths: - - ".github/workflows/mobile_ci.yaml" - - "frontend/**" - - "!frontend/appflowy_tauri/**" - - pull_request: - branches: - - "main" - paths: - - ".github/workflows/mobile_ci.yaml" - - "frontend/**" - - "!frontend/appflowy_tauri/**" - -env: - FLUTTER_VERSION: "3.19.0" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build: - if: github.event.pull_request.draft != true - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - include: - - os: ubuntu-latest - target: aarch64-linux-android - runs-on: ${{ matrix.os }} - - steps: - - name: Check storage space - run: df -h - - - name: Maximize build space - uses: easimon/maximize-build-space@master - with: - root-reserve-mb: 2048 - swap-size-mb: 1024 - remove-dotnet: 'true' - - # the following step is required to avoid running out of space - - name: Maximize build space - if: matrix.os == 'ubuntu-latest' - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo docker image prune --all --force - sudo rm -rf /opt/hostedtoolcache/codeQL - sudo rm -rf ${GITHUB_WORKSPACE}/.git - sudo rm -rf $ANDROID_HOME/ndk - - - name: Check storage space - run: df -h - - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: "r24" - add-to-path: true - - - uses: gradle/gradle-build-action@v3 - with: - gradle-version: 7.6.3 - - - uses: davidB/rust-cargo-make@v1 - with: - version: "0.36.6" - - - name: Install prerequisites - working-directory: frontend - run: | - rustup target install aarch64-linux-android - rustup target install x86_64-linux-android - cargo install --force duckscript_cli - cargo install cargo-ndk - if [ "$RUNNER_OS" == "Linux" ]; then - sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub - sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list - sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev - sudo apt-get install keybinder-3.0 libnotify-dev - sudo apt-get install gcc-multilib - elif [ "$RUNNER_OS" == "Windows" ]; then - vcpkg integrate install - elif [ "$RUNNER_OS" == "macOS" ]; then - echo 'do nothing' - fi - cargo make appflowy-flutter-deps-tools - shell: bash - - - name: Build AppFlowy - working-directory: frontend - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - run: | - cargo make --profile development-android appflowy-android-dev-ci diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart index 4d72154053..6f58ba6354 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart @@ -16,9 +16,9 @@ import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; -import '../util/dir.dart'; -import '../util/mock/mock_file_picker.dart'; -import '../util/util.dart'; +import '../shared/dir.dart'; +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart index 11a09b2b47..6abdb968a1 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart @@ -11,10 +11,11 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_v import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; -import '../util/mock/mock_file_picker.dart'; -import '../util/util.dart'; +import 'package:path/path.dart' as p; + +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -47,13 +48,13 @@ void main() { tester.expectToSeeGoogleLoginButton(); }); - testWidgets('sign in as annoymous', (tester) async { + testWidgets('sign in as anonymous', (tester) async { await tester.initializeAppFlowy( cloudType: AuthenticatorType.appflowyCloudSelfHost, ); await tester.tapSignInAsGuest(); - // should not see the sync setting page when sign in as annoymous + // should not see the sync setting page when sign in as anonymous await tester.openSettings(); await tester.openSettingsPage(SettingsPage.user); tester.expectToSeeGoogleLoginButton(); diff --git a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart index b0ad36755b..2435d15c3d 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart @@ -1,8 +1,9 @@ -import 'empty_test.dart' as preset_af_cloud_env_test; +import 'anon_user_continue_test.dart' as anon_user_continue_test; import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test; +import 'collaborative_workspace_test.dart' as collaboration_workspace_test; +import 'empty_test.dart' as preset_af_cloud_env_test; // import 'document_sync_test.dart' as document_sync_test; import 'user_setting_sync_test.dart' as user_sync_test; -import 'anon_user_continue_test.dart' as anon_user_continue_test; Future main() async { preset_af_cloud_env_test.main(); @@ -14,4 +15,6 @@ Future main() async { user_sync_test.main(); anon_user_continue_test.main(); + + collaboration_workspace_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart new file mode 100644 index 0000000000..56d9f2bb2c --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart @@ -0,0 +1,115 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/feature_flags.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../shared/database_test_op.dart'; +import '../shared/dir.dart'; +import '../shared/emoji.dart'; +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + final email = '${uuid()}@appflowy.io'; + + group('collaborative workspace', () { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + + // combine the create and delete workspace test to reduce the time + testWidgets('create a new workspace, open it and then delete it', + (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + email: email, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + const name = 'AppFlowy.IO'; + await tester.createCollaborativeWorkspace(name); + + // see the success message + var success = find.text(LocaleKeys.workspace_createSuccess.tr()); + expect(success, findsOneWidget); + await tester.pumpUntilNotFound(success); + + // check the create result + await tester.openCollaborativeWorkspaceMenu(); + var items = find.byType(WorkspaceMenuItem); + expect(items, findsNWidgets(2)); + expect( + tester.widget(items.last).workspace.name, + name, + ); + + // open the newly created workspace + await tester.tapButton(items.last); + success = find.text(LocaleKeys.workspace_openSuccess.tr()); + expect(success, findsOneWidget); + await tester.pumpUntilNotFound(success); + + await tester.closeCollaborativeWorkspaceMenu(); + + // delete the newly created workspace + await tester.openCollaborativeWorkspaceMenu(); + final secondWorkspace = find.byType(WorkspaceMenuItem).last; + await tester.hoverOnWidget( + secondWorkspace, + onHover: () async { + // click the more button + final moreButton = find.byType(WorkspaceMoreActionList); + expect(moreButton, findsOneWidget); + await tester.tapButton(moreButton); + // click the delete button + final deleteButton = find.text(LocaleKeys.button_delete.tr()); + expect(deleteButton, findsOneWidget); + await tester.tapButton(deleteButton); + // see the delete confirm dialog + final confirm = + find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr()); + expect(confirm, findsOneWidget); + await tester.tapButton(find.text(LocaleKeys.button_ok.tr())); + // delete success + success = find.text(LocaleKeys.workspace_createSuccess.tr()); + expect(success, findsOneWidget); + await tester.pumpUntilNotFound(success); + }, + ); + + // check the result + await tester.openCollaborativeWorkspaceMenu(); + items = find.byType(WorkspaceMenuItem); + expect(items, findsOneWidget); + expect( + tester.widget(items.last).workspace.name != name, + true, + ); + await tester.closeCollaborativeWorkspaceMenu(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart index 5ad38e49e2..e727e0bcec 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart @@ -16,9 +16,9 @@ import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; -import '../util/dir.dart'; -import '../util/mock/mock_file_picker.dart'; -import '../util/util.dart'; +import '../shared/dir.dart'; +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/cloud/empty_test.dart b/frontend/appflowy_flutter/integration_test/cloud/empty_test.dart index 0ec336ae51..9f7d3ce9ed 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/empty_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/empty_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../shared/util.dart'; // This test is meaningless, just for preventing the CI from failing. void main() { diff --git a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart index 2a3b0bd3f8..283e55ce4e 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart @@ -6,7 +6,8 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_v import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; + +import '../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -35,11 +36,11 @@ void main() { tester.expectToSeeGoogleLoginButton(); }); - testWidgets('sign in as annoymous', (tester) async { + testWidgets('sign in as anonymous', (tester) async { await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); await tester.tapSignInAsGuest(); - // should not see the sync setting page when sign in as annoymous + // should not see the sync setting page when sign in as anonymous await tester.openSettings(); await tester.openSettingsPage(SettingsPage.user); tester.expectToSeeGoogleLoginButton(); diff --git a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart index b83d0f3cf4..1727669876 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart @@ -18,11 +18,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/dir.dart'; -import '../util/emoji.dart'; -import '../util/mock/mock_file_picker.dart'; -import '../util/util.dart'; +import '../shared/database_test_op.dart'; +import '../shared/dir.dart'; +import '../shared/emoji.dart'; +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_add_row_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/board/board_add_row_test.dart index e6e098f000..4159f679ad 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_add_row_test.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; const defaultFirstCardName = 'Card 1'; const defaultLastCardName = 'Card 3'; diff --git a/frontend/appflowy_flutter/integration_test/board/board_group_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/board/board_group_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart index 1ee1778e9c..314da581d9 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_group_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:appflowy_board/appflowy_board.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart index 0271a2b559..27c8efb511 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart @@ -7,8 +7,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/board/board_row_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart index fae4e53482..3916730b2f 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart @@ -7,8 +7,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; -import '../util/database_test_op.dart'; +import '../../shared/util.dart'; +import '../../shared/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/board/board_test_runner.dart rename to frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart diff --git a/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart index 33f06557be..adfb670fe2 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart @@ -5,8 +5,8 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/database/database_cell_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart index d98e063bcc..1ce8a8a3fa 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart @@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:intl/intl.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_field_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart similarity index 96% rename from frontend/appflowy_flutter/integration_test/database/database_field_settings_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart index 1dc39838a5..a7b92e7a0e 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_field_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart @@ -4,8 +4,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/database/database_field_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart index 5a41bbd9d5..22838adddd 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart @@ -8,8 +8,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:intl/intl.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_filter_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/database/database_filter_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart index a7f5726842..4dfc64ff62 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_filter_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart @@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum. import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; +import '../../shared/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_reminder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_reminder_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/database/database_reminder_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_reminder_test.dart index 33b6eb67c7..35ff31d30f 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_reminder_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_reminder_test.dart @@ -5,8 +5,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/database/database_row_page_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart index 6bc5da5c5f..23dabda97c 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_row_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart @@ -7,9 +7,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/emoji.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/emoji.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart similarity index 96% rename from frontend/appflowy_flutter/integration_test/database/database_row_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart index 61d90e72a9..93674ca52f 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart @@ -2,8 +2,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_setting_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_setting_test.dart similarity index 95% rename from frontend/appflowy_flutter/integration_test/database/database_setting_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_setting_test.dart index caa5791e4c..0830a960c7 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_setting_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_setting_test.dart @@ -3,8 +3,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_share_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/database/database_share_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart index 51ede61930..bd3adff7cc 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_share_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum. import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; +import '../../shared/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_sort_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/database/database_sort_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart index 6b4d53d5bc..e28072cebc 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_sort_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum. import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; +import '../../shared/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/database/database_view_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/database/database_view_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart index 1a2e101763..e107f608f0 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_view_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart @@ -3,8 +3,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/document/document_alignment_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index 2bf621dda0..99cc3e7b39 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_alignment_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart @@ -5,8 +5,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/keyboard.dart'; -import '../util/util.dart'; +import '../../shared/keyboard.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_codeblock_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_codeblock_paste_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart index 3ebe006961..cd6d960ee0 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_codeblock_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index aab5f41b5b..6b4512c348 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -8,7 +8,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart index d0939d9557..65a5c50fe2 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart index 7104e7e7bc..4c3937ce54 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart @@ -1,14 +1,13 @@ -import 'package:flutter/services.dart'; - import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/keyboard.dart'; -import '../util/util.dart'; +import '../../shared/keyboard.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_option_action_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_option_action_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart index da09508936..6912ffff85 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_option_action_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/document/document_test_runner.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart diff --git a/frontend/appflowy_flutter/integration_test/document/document_text_direction_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_text_direction_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_text_direction_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_text_direction_test.dart index 688dba2894..d7e710c52f 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_text_direction_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_text_direction_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart index 5c8f4cf184..7d7d437418 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart @@ -7,8 +7,8 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/emoji.dart'; -import '../util/util.dart'; +import '../../shared/emoji.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart index 6f4fe79d4c..593977b159 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart @@ -1,9 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; -import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; +import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -12,7 +12,7 @@ import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_image_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_with_image_block_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart index f3dbf3948e..ca93ac1677 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_image_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart @@ -22,8 +22,8 @@ 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 '../util/mock/mock_file_picker.dart'; -import '../util/util.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart index c1a7fef063..a1d1d0e335 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_1.png b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_1.png similarity index 100% rename from frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_1.png rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_1.png diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_2.png b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_2.png similarity index 100% rename from frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test_2.png rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_2.png diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart index 414ffa250d..0078c22b62 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart @@ -5,7 +5,7 @@ import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_link_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_link_test.dart index 400d130666..0673977567 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_link_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart index f45eafbad6..2264067f1c 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart @@ -7,7 +7,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; const String heading1 = "Heading 1"; const String heading2 = "Heading 2"; diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart index e6ce6baff1..786b02ded0 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/document/edit_document_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.dart index 84579b1ea7..3d2d039e92 100644 --- a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/document/edit_document_test.png b/frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.png similarity index 100% rename from frontend/appflowy_flutter/integration_test/document/edit_document_test.png rename to frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.png diff --git a/frontend/appflowy_flutter/integration_test/grid/grid_calculations_test.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_calculations_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/grid/grid_calculations_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/grid/grid_calculations_test.dart index c49d399c62..ad9cffd304 100644 --- a/frontend/appflowy_flutter/integration_test/grid/grid_calculations_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_calculations_test.dart @@ -4,8 +4,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/database_test_op.dart'; -import '../util/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/reminder/document_reminder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart similarity index 96% rename from frontend/appflowy_flutter/integration_test/reminder/document_reminder_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart index 85c3d68265..2fc8708890 100644 --- a/frontend/appflowy_flutter/integration_test/reminder/document_reminder_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart @@ -1,5 +1,3 @@ -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; @@ -11,14 +9,15 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/base.dart'; -import '../util/common_operations.dart'; -import '../util/editor_test_operations.dart'; -import '../util/expectation.dart'; -import '../util/keyboard.dart'; +import '../../shared/base.dart'; +import '../../shared/common_operations.dart'; +import '../../shared/editor_test_operations.dart'; +import '../../shared/expectation.dart'; +import '../../shared/keyboard.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart index ab7458b250..039b250b66 100644 --- a/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/settings/settings_runner.dart rename to frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart diff --git a/frontend/appflowy_flutter/integration_test/settings/user_language_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/user_language_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/settings/user_language_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/settings/user_language_test.dart index 32be781749..9904d92e9e 100644 --- a/frontend/appflowy_flutter/integration_test/settings/user_language_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/user_language_test.dart @@ -1,10 +1,11 @@ import 'dart:ui'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import '../util/util.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/sidebar/rename_current_item_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/rename_current_item_test.dart similarity index 93% rename from frontend/appflowy_flutter/integration_test/sidebar/rename_current_item_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/sidebar/rename_current_item_test.dart index 0740b76aa7..4ed430f77f 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/rename_current_item_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/rename_current_item_test.dart @@ -6,9 +6,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/base.dart'; -import '../util/common_operations.dart'; -import '../util/keyboard.dart'; +import '../../shared/keyboard.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/sidebar/sidebar_expand_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart index 76d183d625..7568a81def 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_expand_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart @@ -6,7 +6,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart index 81bee64689..aa4f151ab8 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart @@ -6,9 +6,9 @@ import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/base.dart'; -import '../util/common_operations.dart'; -import '../util/expectation.dart'; +import '../../shared/base.dart'; +import '../../shared/common_operations.dart'; +import '../../shared/expectation.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart similarity index 93% rename from frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart index a2a641ec8c..36690b1a09 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart @@ -2,9 +2,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/base.dart'; -import '../util/common_operations.dart'; -import '../util/expectation.dart'; +import '../../shared/base.dart'; +import '../../shared/common_operations.dart'; +import '../../shared/expectation.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart similarity index 99% rename from frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart index 1da433559c..eb62736022 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart @@ -12,7 +12,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart rename to frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart diff --git a/frontend/appflowy_flutter/integration_test/appearance_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/appearance_settings_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/appearance_settings_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/appearance_settings_test.dart index 4d01a07ab9..60aed323d0 100644 --- a/frontend/appflowy_flutter/integration_test/appearance_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/appearance_settings_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/board_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/board_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/board_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/board_test.dart index a5d329515d..5e88b38697 100644 --- a/frontend/appflowy_flutter/integration_test/board_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/board_test.dart @@ -1,7 +1,8 @@ import 'package:appflowy_board/appflowy_board.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/util.dart'; + +import '../../shared/util.dart'; /// Integration tests for an empty board. The [TestWorkspaceService] will load /// a workspace from an empty board `assets/test/workspaces/board.zip` for all diff --git a/frontend/appflowy_flutter/integration_test/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart similarity index 94% rename from frontend/appflowy_flutter/integration_test/emoji_shortcut_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart index 4bc15dd214..aaf6a69adb 100644 --- a/frontend/appflowy_flutter/integration_test/emoji_shortcut_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; + 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'; @@ -6,8 +7,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/keyboard.dart'; -import 'util/util.dart'; +import '../../shared/keyboard.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -17,7 +18,7 @@ void main() { (tester) async { await tester.initializeAppFlowy(); await tester.tapGoButton(); - + final Finder editor = find.byType(AppFlowyEditor); await tester.tap(editor); await tester.pumpAndSettle(); diff --git a/frontend/appflowy_flutter/integration_test/empty_document_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_document_test.dart similarity index 98% rename from frontend/appflowy_flutter/integration_test/empty_document_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_document_test.dart index 273e7bb73a..6712e6959d 100644 --- a/frontend/appflowy_flutter/integration_test/empty_document_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_document_test.dart @@ -3,8 +3,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/keyboard.dart'; -import 'util/util.dart'; + +import '../../shared/keyboard.dart'; +import '../../shared/util.dart'; /// Integration tests for an empty document. The [TestWorkspaceService] will load a workspace from an empty document `assets/test/workspaces/empty_document.zip` for all tests. /// diff --git a/frontend/appflowy_flutter/integration_test/empty_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart similarity index 92% rename from frontend/appflowy_flutter/integration_test/empty_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart index 0b66c41213..86d44add0e 100644 --- a/frontend/appflowy_flutter/integration_test/empty_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/util.dart'; +import '../../shared/util.dart'; // This test is meaningless, just for preventing the CI from failing. void main() { diff --git a/frontend/appflowy_flutter/integration_test/hotkeys_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/hotkeys_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart index cf55e183ca..cfe1e80ce8 100644 --- a/frontend/appflowy_flutter/integration_test/hotkeys_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart'; @@ -8,8 +9,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/keyboard.dart'; -import 'util/util.dart'; +import '../../shared/keyboard.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/import_files_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart similarity index 94% rename from frontend/appflowy_flutter/integration_test/import_files_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart index 8fe3113354..58d8cc75be 100644 --- a/frontend/appflowy_flutter/integration_test/import_files_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; - -import 'util/mock/mock_file_picker.dart'; -import 'util/util.dart'; import 'package:path/path.dart' as p; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/language_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/language_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/language_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/language_test.dart index 94aeef0e9d..c48fcd8028 100644 --- a/frontend/appflowy_flutter/integration_test/language_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/language_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_langua import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/util.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart index 969f9ec60b..f739820d04 100644 --- a/frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart @@ -7,8 +7,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/mock/mock_openai_repository.dart'; -import 'util/util.dart'; +import '../../shared/mock/mock_openai_repository.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/share_markdown_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart index 689ec4d0ed..7ba09445a9 100644 --- a/frontend/appflowy_flutter/integration_test/share_markdown_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart @@ -5,8 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; -import 'util/mock/mock_file_picker.dart'; -import 'util/util.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/switch_folder_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart index 9cd9c9a374..e2a343d4f1 100644 --- a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart @@ -7,8 +7,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; -import 'util/mock/mock_file_picker.dart'; -import 'util/util.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/frontend/appflowy_flutter/integration_test/tabs_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart similarity index 93% rename from frontend/appflowy_flutter/integration_test/tabs_test.dart rename to frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart index 4c3d2fed16..87a28dc923 100644 --- a/frontend/appflowy_flutter/integration_test/tabs_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart @@ -8,10 +8,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'util/base.dart'; -import 'util/common_operations.dart'; -import 'util/expectation.dart'; -import 'util/keyboard.dart'; +import '../../shared/base.dart'; +import '../../shared/common_operations.dart'; +import '../../shared/expectation.dart'; +import '../../shared/keyboard.dart'; const _documentName = 'First Doc'; const _documentTwoName = 'Second Doc'; diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner.dart b/frontend/appflowy_flutter/integration_test/desktop_runner.dart new file mode 100644 index 0000000000..66532d947c --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop_runner.dart @@ -0,0 +1,68 @@ +import 'desktop/uncategorized/appearance_settings_test.dart' as appearance_test_runner; +import 'desktop/board/board_test_runner.dart' as board_test_runner; +import 'desktop/database/database_calendar_test.dart' as database_calendar_test; +import 'desktop/database/database_cell_test.dart' as database_cell_test; +import 'desktop/database/database_field_settings_test.dart' + as database_field_settings_test; +import 'desktop/database/database_field_test.dart' as database_field_test; +import 'desktop/database/database_filter_test.dart' as database_filter_test; +import 'desktop/database/database_row_page_test.dart' as database_row_page_test; +import 'desktop/database/database_row_test.dart' as database_row_test; +import 'desktop/database/database_setting_test.dart' as database_setting_test; +import 'desktop/database/database_share_test.dart' as database_share_test; +import 'desktop/database/database_sort_test.dart' as database_sort_test; +import 'desktop/database/database_view_test.dart' as database_view_test; +import 'desktop/document/document_test_runner.dart' as document_test_runner; +import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test; +import 'desktop/uncategorized/empty_test.dart' as first_test; +import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test; +import 'desktop/uncategorized/import_files_test.dart' as import_files_test; +import 'desktop/settings/settings_runner.dart' as settings_test_runner; +import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test; +import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner; +import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test; +import 'desktop/uncategorized/tabs_test.dart' as tabs_test; + +Future runIntegrationOnDesktop() async { + // This test must be run first, otherwise the CI will fail. + first_test.main(); + + switch_folder_test.main(); + share_markdown_test.main(); + import_files_test.main(); + + // Document integration tests + document_test_runner.startTesting(); + + // Sidebar integration tests + sidebar_test_runner.startTesting(); + + // Board integration test + board_test_runner.startTesting(); + + // Database integration tests + database_cell_test.main(); + database_field_test.main(); + database_field_settings_test.main(); + database_share_test.main(); + database_row_page_test.main(); + database_row_test.main(); + database_setting_test.main(); + database_filter_test.main(); + database_sort_test.main(); + database_view_test.main(); + database_calendar_test.main(); + + // Tabs + tabs_test.main(); + + // Others + hotkeys_test.main(); + emoji_shortcut_test.main(); + + // Appearance integration test + appearance_test_runner.main(); + + // User settings + settings_test_runner.main(); +} diff --git a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart new file mode 100644 index 0000000000..f90b151372 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart @@ -0,0 +1,44 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/home/home.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../../shared/dir.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('anonymous sign in on mobile', () { + testWidgets('anon user and then sign in', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.local, + ); + + // click the anonymousSignInButton + final anonymousSignInButton = find.byType(SignInAnonymousButton); + expect(anonymousSignInButton, findsOneWidget); + await tester.tapButton(anonymousSignInButton); + + // expect to see the home page + expect(find.byType(MobileHomeScreen), findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/mobile_runner.dart b/frontend/appflowy_flutter/integration_test/mobile_runner.dart new file mode 100644 index 0000000000..daab343060 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/mobile_runner.dart @@ -0,0 +1,5 @@ +import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test; + +Future runIntegrationOnMobile() async { + anonymous_sign_in_test.main(); +} diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index 51abc6de7b..39f0bf45e1 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -1,30 +1,9 @@ +import 'dart:io'; + import 'package:integration_test/integration_test.dart'; -import 'appearance_settings_test.dart' as appearance_test_runner; -import 'board/board_test_runner.dart' as board_test_runner; -import 'database/database_calendar_test.dart' as database_calendar_test; -import 'database/database_cell_test.dart' as database_cell_test; -import 'database/database_field_settings_test.dart' - as database_field_settings_test; -import 'database/database_field_test.dart' as database_field_test; -import 'database/database_filter_test.dart' as database_filter_test; -import 'database/database_row_page_test.dart' as database_row_page_test; -import 'database/database_row_test.dart' as database_row_test; -import 'database/database_setting_test.dart' as database_setting_test; -import 'database/database_share_test.dart' as database_share_test; -import 'database/database_sort_test.dart' as database_sort_test; -import 'database/database_view_test.dart' as database_view_test; -import 'document/document_test_runner.dart' as document_test_runner; -import 'empty_test.dart' as first_test; -import 'hotkeys_test.dart' as hotkeys_test; -import 'import_files_test.dart' as import_files_test; -import 'settings/settings_runner.dart' as settings_test_runner; -import 'share_markdown_test.dart' as share_markdown_test; -import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner; -import 'switch_folder_test.dart' as switch_folder_test; -import 'tabs_test.dart' as tabs_test; -import 'emoji_shortcut_test.dart' as emoji_shortcut_test; -// import 'auth/supabase_auth_test.dart' as supabase_auth_test_runner; +import 'desktop_runner.dart'; +import 'mobile_runner.dart'; /// The main task runner for all integration tests in AppFlowy. /// @@ -35,50 +14,11 @@ import 'emoji_shortcut_test.dart' as emoji_shortcut_test; /// as the test target. Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // This test must be run first, otherwise the CI will fail. - first_test.main(); - - switch_folder_test.main(); - share_markdown_test.main(); - import_files_test.main(); - - // Document integration tests - document_test_runner.startTesting(); - - // Sidebar integration tests - sidebar_test_runner.startTesting(); - - // Board integration test - board_test_runner.startTesting(); - - // Database integration tests - database_cell_test.main(); - database_field_test.main(); - database_field_settings_test.main(); - database_share_test.main(); - database_row_page_test.main(); - database_row_test.main(); - database_setting_test.main(); - database_filter_test.main(); - database_sort_test.main(); - database_view_test.main(); - database_calendar_test.main(); - - // Tabs - tabs_test.main(); - - // Others - hotkeys_test.main(); - emoji_shortcut_test.main(); - - // Appearance integration test - appearance_test_runner.main(); - - // User settings - settings_test_runner.main(); - - // board_test.main(); - // empty_document_test.main(); - // smart_menu_test.main(); + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + await runIntegrationOnDesktop(); + } else if (Platform.isIOS || Platform.isAndroid) { + await runIntegrationOnMobile(); + } else { + throw Exception('Unsupported platform'); + } } diff --git a/frontend/appflowy_flutter/integration_test/util/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/auth_operation.dart rename to frontend/appflowy_flutter/integration_test/shared/auth_operation.dart diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart similarity index 90% rename from frontend/appflowy_flutter/integration_test/util/base.dart rename to frontend/appflowy_flutter/integration_test/shared/base.dart index 11b0dcf1ba..0feb050188 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -37,9 +37,10 @@ extension AppFlowyTestBase on WidgetTester { AuthenticatorType? cloudType, String? email, }) async { - // view.physicalSize = windowsSize; - await binding.setSurfaceSize(windowSize); - // addTearDown(() => binding.setSurfaceSize(null)); + if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + // Set the window size + await binding.setSurfaceSize(windowSize); + } mockHotKeyManagerHandlers(); final applicationDataDirectory = dataDirectory ?? @@ -118,7 +119,7 @@ extension AppFlowyTestBase on WidgetTester { Future waitUntilSignInPageShow() async { if (isAuthEnabled) { final finder = find.byType(SignInAnonymousButton); - await pumpUntilFound(finder); + await pumpUntilFound(finder, timeout: const Duration(seconds: 30)); expect(finder, findsOneWidget); } else { final finder = find.byType(GoButton); @@ -134,8 +135,9 @@ extension AppFlowyTestBase on WidgetTester { Future pumpUntilFound( Finder finder, { Duration timeout = const Duration(seconds: 10), - Duration pumpInterval = - const Duration(milliseconds: 50), // Interval between pumps + Duration pumpInterval = const Duration( + milliseconds: 50, + ), // Interval between pumps }) async { bool timerDone = false; final timer = Timer(timeout, () => timerDone = true); @@ -148,6 +150,24 @@ extension AppFlowyTestBase on WidgetTester { timer.cancel(); } + Future pumpUntilNotFound( + Finder finder, { + Duration timeout = const Duration(seconds: 10), + Duration pumpInterval = const Duration( + milliseconds: 50, + ), // Interval between pumps + }) async { + bool timerDone = false; + final timer = Timer(timeout, () => timerDone = true); + while (!timerDone) { + await pump(pumpInterval); // Pump with an interval + if (!any(finder)) { + break; + } + } + timer.cancel(); + } + Future tapButton( Finder finder, { int? pointer, diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart similarity index 90% rename from frontend/appflowy_flutter/integration_test/util/common_operations.dart rename to frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 12b38cf78a..ab9cbdc0d0 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -6,10 +6,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; +import 'package:appflowy/shared/feature_flags.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/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; @@ -518,6 +521,51 @@ extension CommonOperations on WidgetTester { await pumpAndSettle(); } } + + Future openCollaborativeWorkspaceMenu() async { + if (!FeatureFlag.collaborativeWorkspace.isOn) { + throw UnsupportedError('Collaborative workspace is not enabled'); + } + + final workspace = find.byType(SidebarWorkspace); + expect(workspace, findsOneWidget); + // click it + await tapButton(workspace); + } + + Future closeCollaborativeWorkspaceMenu() async { + if (!FeatureFlag.collaborativeWorkspace.isOn) { + throw UnsupportedError('Collaborative workspace is not enabled'); + } + + await tapAt(Offset.zero); + await pumpAndSettle(); + } + + Future createCollaborativeWorkspace(String name) async { + if (!FeatureFlag.collaborativeWorkspace.isOn) { + throw UnsupportedError('Collaborative workspace is not enabled'); + } + await openCollaborativeWorkspaceMenu(); + // expect to see the workspace list, and there should be only one workspace + final workspacesMenu = find.byType(WorkspacesMenu); + expect(workspacesMenu, findsOneWidget); + + // click the create button + final createButton = find.byKey(createWorkspaceButtonKey); + expect(createButton, findsOneWidget); + await tapButton(createButton); + + // see the create workspace dialog + final createWorkspaceDialog = find.byType(CreateWorkspaceDialog); + expect(createWorkspaceDialog, findsOneWidget); + + // input the workspace name + await enterText(find.byType(TextField), name); + await pumpAndSettle(); + + await tapButtonWithName(LocaleKeys.button_ok.tr()); + } } extension ViewLayoutPBTest on ViewLayoutPB { diff --git a/frontend/appflowy_flutter/integration_test/util/data.dart b/frontend/appflowy_flutter/integration_test/shared/data.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/data.dart rename to frontend/appflowy_flutter/integration_test/shared/data.dart diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/database_test_op.dart rename to frontend/appflowy_flutter/integration_test/shared/database_test_op.dart diff --git a/frontend/appflowy_flutter/integration_test/util/dir.dart b/frontend/appflowy_flutter/integration_test/shared/dir.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/dir.dart rename to frontend/appflowy_flutter/integration_test/shared/dir.dart diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart rename to frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart diff --git a/frontend/appflowy_flutter/integration_test/util/emoji.dart b/frontend/appflowy_flutter/integration_test/shared/emoji.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/emoji.dart rename to frontend/appflowy_flutter/integration_test/shared/emoji.dart diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/expectation.dart rename to frontend/appflowy_flutter/integration_test/shared/expectation.dart diff --git a/frontend/appflowy_flutter/integration_test/util/ime.dart b/frontend/appflowy_flutter/integration_test/shared/ime.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/ime.dart rename to frontend/appflowy_flutter/integration_test/shared/ime.dart diff --git a/frontend/appflowy_flutter/integration_test/util/keyboard.dart b/frontend/appflowy_flutter/integration_test/shared/keyboard.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/keyboard.dart rename to frontend/appflowy_flutter/integration_test/shared/keyboard.dart diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart rename to frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart rename to frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_url_launcher.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/mock/mock_url_launcher.dart rename to frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart diff --git a/frontend/appflowy_flutter/integration_test/util/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/settings.dart rename to frontend/appflowy_flutter/integration_test/shared/settings.dart diff --git a/frontend/appflowy_flutter/integration_test/util/util.dart b/frontend/appflowy_flutter/integration_test/shared/util.dart similarity index 100% rename from frontend/appflowy_flutter/integration_test/util/util.dart rename to frontend/appflowy_flutter/integration_test/shared/util.dart diff --git a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart index e91549641e..9aba14cd27 100644 --- a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart +++ b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; -class NotificationParser { +class NotificationParser { NotificationParser({ this.id, required this.callback, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart index 1d5fce1466..d27085b394 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart @@ -20,6 +20,7 @@ class OptionColorList extends StatelessWidget { crossAxisCount: 6, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, children: SelectOptionColorPB.values.map( (colorPB) { final color = colorPB.toColor(context); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index bf94d766f2..a240f32178 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -65,7 +65,7 @@ Future showMobileBottomSheet( backgroundColor ??= Theme.of(context).brightness == Brightness.light ? const Color(0xFFF7F8FB) - : const Color(0xFF626364); + : const Color(0xFF23262B); return showModalBottomSheet( context: context, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart index ed1130de1d..d9b9127468 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart @@ -39,7 +39,7 @@ Future showTransitionMobileBottomSheet( backgroundColor ??= Theme.of(context).brightness == Brightness.light ? const Color(0xFFF7F8FB) - : const Color(0xFF626364); + : const Color(0xFF23262B); return Navigator.of( context, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart index eab036aabc..301c375c4b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart @@ -55,7 +55,7 @@ class _MobileDateCellEditScreenState extends State { minChildSize: 0.4, snapSizes: const [0.4, 0.7, 1.0], builder: (_, controller) => Material( - color: Theme.of(context).colorScheme.secondaryContainer, + color: Colors.transparent, child: ListView( controller: controller, children: [ diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart index 6c1a1b758a..bd539f4821 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart @@ -47,7 +47,9 @@ Future showFieldTypeGridBottomSheet( .map( (fieldType) => TypeOptionMenuItemValue( value: fieldType, - backgroundColor: fieldType.mobileIconBackgroundColor, + backgroundColor: Theme.of(context).brightness == Brightness.light + ? fieldType.mobileIconBackgroundColor + : fieldType.mobileIconBackgroundColorDark, text: fieldType.i18n, icon: fieldType.svgData, onTap: (context, fieldType) => @@ -121,7 +123,6 @@ void showQuickEditField( ) { showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, showDragHandle: true, builder: (context) { return SingleChildScrollView( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart index 469dc07a1c..95c9fd0ee0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart @@ -208,7 +208,9 @@ class _MobileFieldEditorState extends State { Widget build(BuildContext context) { final option = _buildOption(); return Container( - color: Theme.of(context).colorScheme.secondaryContainer, + color: Theme.of(context).brightness == Brightness.light + ? const Color(0xFFF7F8FB) + : const Color(0xFF23262B), height: MediaQuery.of(context).size.height, child: SingleChildScrollView( child: Column( @@ -259,6 +261,7 @@ class _MobileFieldEditorState extends State { ], ..._buildOptionActions(), const _Divider(), + VSpace(MediaQuery.viewPaddingOf(context).bottom == 0 ? 28.0 : 16.0), ], ), ), @@ -351,7 +354,7 @@ class _MobileFieldEditorState extends State { } return [ - if (widget.actions.contains(FieldOptionAction.hide)) + if (widget.actions.contains(FieldOptionAction.hide) && !widget.isPrimary) FlowyOptionTile.text( text: LocaleKeys.grid_field_hide.tr(), leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart index 5c19b3d6be..d7d9b7993f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart @@ -3,7 +3,7 @@ import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -27,10 +27,13 @@ class MobileFavoritePageFolder extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => MenuBloc( - user: userProfile, - workspaceId: workspaceSetting.workspaceId, - )..add(const MenuEvent.initial()), + create: (_) => SidebarRootViewsBloc() + ..add( + SidebarRootViewsEvent.initial( + userProfile, + workspaceSetting.workspaceId, + ), + ), ), BlocProvider( create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), @@ -38,11 +41,11 @@ class MobileFavoritePageFolder extends StatelessWidget { ], child: MultiBlocListener( listeners: [ - BlocListener( + BlocListener( listenWhen: (p, c) => - p.lastCreatedView?.id != c.lastCreatedView?.id, + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, listener: (context, state) => - context.pushView(state.lastCreatedView!), + context.pushView(state.lastCreatedRootView!), ), ], child: Builder( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index da58fb8f5c..cc42b3c9b9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -1,7 +1,7 @@ import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -26,10 +26,13 @@ class MobileFolders extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => MenuBloc( - user: user, - workspaceId: workspaceSetting.workspaceId, - )..add(const MenuEvent.initial()), + create: (_) => SidebarRootViewsBloc() + ..add( + SidebarRootViewsEvent.initial( + user, + workspaceSetting.workspaceId, + ), + ), ), BlocProvider( create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), @@ -37,16 +40,16 @@ class MobileFolders extends StatelessWidget { ], child: MultiBlocListener( listeners: [ - BlocListener( + BlocListener( listenWhen: (p, c) => - p.lastCreatedView?.id != c.lastCreatedView?.id, + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, listener: (context, state) => - context.pushView(state.lastCreatedView!), + context.pushView(state.lastCreatedRootView!), ), ], child: Builder( builder: (context) { - final menuState = context.watch().state; + final menuState = context.watch().state; return SlidableAutoCloseBehavior( child: Column( children: [ diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart index 91baaf4f68..6e77f86454 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -67,8 +67,8 @@ class _MobilePersonalFolderHeaderState size: Size.square(iconSize), ), onPressed: () { - context.read().add( - MenuEvent.createApp( + context.read().add( + SidebarRootViewsEvent.createRootView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), index: 0, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart index 8156e8debe..f87ef8645f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart @@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart'; @@ -80,11 +80,14 @@ class _NotificationScreenContent extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => MenuBloc( - workspaceId: workspaceSetting.workspaceId, - user: userProfile, - )..add(const MenuEvent.initial()), - child: BlocBuilder( + create: (_) => SidebarRootViewsBloc() + ..add( + SidebarRootViewsEvent.initial( + userProfile, + workspaceSetting.workspaceId, + ), + ), + child: BlocBuilder( builder: (context, menuState) => BlocBuilder( builder: (context, filterState) => diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart index 2665c4da1d..55394ec33c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart @@ -87,7 +87,11 @@ class _DatabaseViewSelectorButton extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(12)), ), ), - backgroundColor: const MaterialStatePropertyAll(Color(0x0F212729)), + backgroundColor: MaterialStatePropertyAll( + Theme.of(context).brightness == Brightness.light + ? const Color(0x0F212729) + : const Color(0x0FFFFFFF), + ), overlayColor: MaterialStatePropertyAll( Theme.of(context).colorScheme.secondary, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart index 49fbf2639b..aa55dd9e36 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart @@ -39,7 +39,6 @@ class MobileGridChecklistCellSkin extends IEditableChecklistCellSkin { ), onTap: () => showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.background, builder: (context) { return BlocProvider.value( value: bloc, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart index fba6937a58..7984322328 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart @@ -44,7 +44,6 @@ class MobileGridDateCellSkin extends IEditableDateCellSkin { onTap: () { showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, builder: (context) { return MobileDateCellEditScreen( controller: bloc.cellController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart index f775896aec..fb8b11474c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart @@ -230,7 +230,7 @@ class _ChecklistItemState extends State<_ChecklistItem> { void _showDeleteTaskBottomSheet() { showMobileBottomSheet( context, - padding: const EdgeInsets.only(top: 8, bottom: 32), + showDragHandle: true, builder: (_) => Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart index 2bded1e077..f7c1c6d67d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart @@ -421,7 +421,6 @@ class _MoreOptionsState extends State<_MoreOptions> { @override Widget build(BuildContext context) { - final color = Theme.of(context).colorScheme.secondaryContainer; return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -432,22 +431,18 @@ class _MoreOptionsState extends State<_MoreOptions> { const VSpace(16.0), Padding( padding: const EdgeInsets.only(left: 12.0), - child: ColoredBox( - color: color, - child: FlowyText( - LocaleKeys.grid_selectOption_colorPanelTitle.tr().toUpperCase(), - color: Theme.of(context).hintColor, - fontSize: 13, - ), + child: FlowyText( + LocaleKeys.grid_selectOption_colorPanelTitle.tr().toUpperCase(), + color: Theme.of(context).hintColor, + fontSize: 13, ), ), const VSpace(4.0), FlowyOptionDecorateBox( child: Padding( - padding: const EdgeInsets.only( - top: 12.0, - left: 6.0, - right: 6.0, + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 6.0, ), child: OptionColorList( selectedColor: option.color, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart index fc1c1140ef..7e6c9de9cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart @@ -147,7 +147,6 @@ void _showEditSortPanelFromToolbar( ) { showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.surface, showDragHandle: true, showDivider: false, useSafeArea: false, diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 1ef6d132e0..34673ae4f4 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -130,4 +130,24 @@ class UserBackendService { final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId; return UserEventDeleteWorkspace(request).send(); } + + Future> renameWorkspace( + String workspaceId, + String name, + ) { + final request = RenameWorkspacePB() + ..workspaceId = workspaceId + ..newName = name; + return UserEventRenameWorkspace(request).send(); + } + + Future> updateWorkspaceIcon( + String workspaceId, + String icon, + ) { + final request = ChangeWorkspaceIconPB() + ..workspaceId = workspaceId + ..newIcon = icon; + return UserEventChangeWorkspaceIcon(request).send(); + } } diff --git a/frontend/appflowy_flutter/lib/util/field_type_extension.dart b/frontend/appflowy_flutter/lib/util/field_type_extension.dart index 67093f401e..517520408a 100644 --- a/frontend/appflowy_flutter/lib/util/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/util/field_type_extension.dart @@ -42,14 +42,30 @@ extension FieldTypeExtension on FieldType { Color get mobileIconBackgroundColor => switch (this) { FieldType.RichText => const Color(0xFFBECCFF), FieldType.Number => const Color(0xFFCABDFF), - FieldType.DateTime => const Color(0xFFFDEDA7), + FieldType.URL => const Color(0xFFFFB9EF), FieldType.SingleSelect => const Color(0xFFBECCFF), FieldType.MultiSelect => const Color(0xFFBECCFF), - FieldType.URL => const Color(0xFFFFB9EF), - FieldType.Checkbox => const Color(0xFF98F4CD), - FieldType.Checklist => const Color(0xFF98F4CD), + FieldType.DateTime => const Color(0xFFFDEDA7), FieldType.LastEditedTime => const Color(0xFFFDEDA7), FieldType.CreatedTime => const Color(0xFFFDEDA7), + FieldType.Checkbox => const Color(0xFF98F4CD), + FieldType.Checklist => const Color(0xFF98F4CD), + FieldType.Relation => const Color(0xFFFDEDA7), + _ => throw UnimplementedError(), + }; + + // TODO(RS): inner icon color isn't always white + Color get mobileIconBackgroundColorDark => switch (this) { + FieldType.RichText => const Color(0xFF6859A7), + FieldType.Number => const Color(0xFF6859A7), + FieldType.URL => const Color(0xFFA75C96), + FieldType.SingleSelect => const Color(0xFF5366AB), + FieldType.MultiSelect => const Color(0xFF5366AB), + FieldType.DateTime => const Color(0xFFB0A26D), + FieldType.LastEditedTime => const Color(0xFFB0A26D), + FieldType.CreatedTime => const Color(0xFFB0A26D), + FieldType.Checkbox => const Color(0xFF42AD93), + FieldType.Checklist => const Color(0xFF42AD93), FieldType.Relation => const Color(0xFFFDEDA7), _ => throw UnimplementedError(), }; diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart deleted file mode 100644 index c9ca94e0ef..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; -import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'menu_bloc.freezed.dart'; - -class MenuBloc extends Bloc { - MenuBloc({required this.user, required this.workspaceId}) - : _workspaceService = WorkspaceService(workspaceId: workspaceId), - _listener = WorkspaceListener( - user: user, - workspaceId: workspaceId, - ), - super(MenuState.initial()) { - _dispatch(); - } - - final WorkspaceService _workspaceService; - final WorkspaceListener _listener; - final UserProfilePB user; - final String workspaceId; - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.map( - initial: (e) async { - _listener.start(appsChanged: _handleAppsOrFail); - await _fetchApps(emit); - }, - createApp: (_CreateApp event) async { - final result = await _workspaceService.createApp( - name: event.name, - desc: event.desc, - index: event.index, - ); - result.fold( - (app) => emit(state.copyWith(lastCreatedView: app)), - (error) { - Log.error(error); - emit( - state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ); - }, - ); - }, - didReceiveApps: (e) async { - emit( - e.appsOrFail.fold( - (views) => state.copyWith( - views: views, - successOrFailure: FlowyResult.success(null), - ), - (err) => - state.copyWith(successOrFailure: FlowyResult.failure(err)), - ), - ); - }, - moveApp: (_MoveApp value) { - if (state.views.length > value.fromIndex) { - final view = state.views[value.fromIndex]; - _workspaceService.moveApp( - appId: view.id, - fromIndex: value.fromIndex, - toIndex: value.toIndex, - ); - final apps = List.from(state.views); - - apps.insert(value.toIndex, apps.removeAt(value.fromIndex)); - emit(state.copyWith(views: apps)); - } - }, - ); - }, - ); - } - - // ignore: unused_element - Future _fetchApps(Emitter emit) async { - final viewsOrError = await _workspaceService.getViews(); - emit( - viewsOrError.fold( - (views) => state.copyWith(views: views), - (error) { - Log.error(error); - return state.copyWith(successOrFailure: FlowyResult.failure(error)); - }, - ), - ); - } - - void _handleAppsOrFail(FlowyResult, FlowyError> appsOrFail) { - appsOrFail.fold( - (apps) => add(MenuEvent.didReceiveApps(FlowyResult.success(apps))), - (error) => add(MenuEvent.didReceiveApps(FlowyResult.failure(error))), - ); - } -} - -@freezed -class MenuEvent with _$MenuEvent { - const factory MenuEvent.initial() = _Initial; - const factory MenuEvent.createApp(String name, {String? desc, int? index}) = - _CreateApp; - const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp; - const factory MenuEvent.didReceiveApps( - FlowyResult, FlowyError> appsOrFail, - ) = _ReceiveApps; -} - -@freezed -class MenuState with _$MenuState { - const factory MenuState({ - required List views, - required FlowyResult successOrFailure, - ViewPB? lastCreatedView, - }) = _MenuState; - - factory MenuState.initial() => MenuState( - views: [], - successOrFailure: FlowyResult.success(null), - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart index 0bf94ea60b..7d24a56b0c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart @@ -1,2 +1,2 @@ -export 'menu_bloc.dart'; export 'menu_user_bloc.dart'; +export 'sidebar_root_views_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart new file mode 100644 index 0000000000..1ad50401b7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart @@ -0,0 +1,160 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sidebar_root_views_bloc.freezed.dart'; + +class SidebarRootViewsBloc + extends Bloc { + SidebarRootViewsBloc() : super(SidebarRootViewState.initial()) { + _dispatch(); + } + + late WorkspaceService _workspaceService; + WorkspaceListener? _listener; + + @override + Future close() async { + await _listener?.stop(); + return super.close(); + } + + void _dispatch() { + on( + (event, emit) async { + await event.when( + initial: (userProfile, workspaceId) async { + _initial(userProfile, workspaceId); + await _fetchApps(emit); + }, + reset: (userProfile, workspaceId) async { + await _listener?.stop(); + _initial(userProfile, workspaceId); + await _fetchApps(emit); + }, + createRootView: (name, desc, index) async { + final result = await _workspaceService.createApp( + name: name, + desc: desc, + index: index, + ); + result.fold( + (view) => emit(state.copyWith(lastCreatedRootView: view)), + (error) { + Log.error(error); + emit( + state.copyWith( + successOrFailure: FlowyResult.failure(error), + ), + ); + }, + ); + }, + didReceiveViews: (viewsOrFailure) async { + emit( + viewsOrFailure.fold( + (views) => state.copyWith( + views: views, + successOrFailure: FlowyResult.success(null), + ), + (err) => + state.copyWith(successOrFailure: FlowyResult.failure(err)), + ), + ); + }, + moveRootView: (int fromIndex, int toIndex) { + if (state.views.length > fromIndex) { + final view = state.views[fromIndex]; + + _workspaceService.moveApp( + appId: view.id, + fromIndex: fromIndex, + toIndex: toIndex, + ); + + final views = List.from(state.views); + views.insert(toIndex, views.removeAt(fromIndex)); + emit(state.copyWith(views: views)); + } + }, + ); + }, + ); + } + + Future _fetchApps(Emitter emit) async { + final viewsOrError = await _workspaceService.getViews(); + emit( + viewsOrError.fold( + (views) => state.copyWith(views: views), + (error) { + Log.error(error); + return state.copyWith(successOrFailure: FlowyResult.failure(error)); + }, + ), + ); + } + + void _handleAppsOrFail(FlowyResult, FlowyError> viewsOrFail) { + viewsOrFail.fold( + (views) => add( + SidebarRootViewsEvent.didReceiveViews(FlowyResult.success(views)), + ), + (error) => add( + SidebarRootViewsEvent.didReceiveViews(FlowyResult.failure(error)), + ), + ); + } + + void _initial(UserProfilePB userProfile, String workspaceId) { + _workspaceService = WorkspaceService(workspaceId: workspaceId); + _listener = WorkspaceListener( + user: userProfile, + workspaceId: workspaceId, + )..start(appsChanged: _handleAppsOrFail); + } +} + +@freezed +class SidebarRootViewsEvent with _$SidebarRootViewsEvent { + const factory SidebarRootViewsEvent.initial( + UserProfilePB userProfile, + String workspaceId, + ) = _Initial; + const factory SidebarRootViewsEvent.reset( + UserProfilePB userProfile, + String workspaceId, + ) = _Reset; + const factory SidebarRootViewsEvent.createRootView( + String name, { + String? desc, + int? index, + }) = _createRootView; + const factory SidebarRootViewsEvent.moveRootView(int fromIndex, int toIndex) = + _MoveRootView; + const factory SidebarRootViewsEvent.didReceiveViews( + FlowyResult, FlowyError> appsOrFail, + ) = _ReceiveApps; +} + +@freezed +class SidebarRootViewState with _$SidebarRootViewState { + const factory SidebarRootViewState({ + required List views, + required FlowyResult successOrFailure, + @Default(null) ViewPB? lastCreatedRootView, + }) = _SidebarRootViewState; + + factory SidebarRootViewState.initial() => SidebarRootViewState( + views: [], + successOrFailure: FlowyResult.success(null), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 8900ae2256..6057ddc8d2 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -74,10 +74,9 @@ class MobileAppearance extends BaseAppearance { outline: _hintColorInDarkMode, outlineVariant: Colors.black, //Snack bar - surface: const Color(0xff2F3030), + surface: const Color(0xFF171A1F), onSurface: const Color(0xffC5C6C7), // text/body color ); - final hintColor = brightness == Brightness.light ? const Color(0x991F2329) : _hintColorInDarkMode; diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 20d9d0ba41..8d33334c8d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -1,10 +1,13 @@ import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; part 'user_workspace_bloc.freezed.dart'; @@ -33,43 +36,207 @@ class UserWorkspaceBloc extends Bloc { }, createWorkspace: (name, desc) async { final result = await _userService.createUserWorkspace(name); + final (workspaces, createWorkspaceResult) = result.fold( + (s) { + final workspaces = [...state.workspaces, s]; + return ( + workspaces, + FlowyResult.success(null) + ); + }, + (e) { + Log.error(e); + return (state.workspaces, FlowyResult.failure(e)); + }, + ); emit( state.copyWith( openWorkspaceResult: null, deleteWorkspaceResult: null, - createWorkspaceResult: - result.fold((s) => FlowyResult.success(null), (e) { - Log.error(e); - return FlowyResult.failure(e); - }), + updateWorkspaceIconResult: null, + createWorkspaceResult: createWorkspaceResult, + workspaces: workspaces, ), ); }, deleteWorkspace: (workspaceId) async { + if (state.workspaces.length <= 1) { + // do not allow to delete the last workspace + return emit( + state.copyWith( + openWorkspaceResult: null, + createWorkspaceResult: null, + updateWorkspaceIconResult: null, + renameWorkspaceResult: null, + deleteWorkspaceResult: FlowyResult.failure( + FlowyError( + code: ErrorCode.Internal, + msg: 'Cannot delete the last workspace', + ), + ), + ), + ); + } + final result = await _userService.deleteWorkspaceById(workspaceId); + final (workspaces, deleteWorkspaceResult) = result.fold( + (s) { + // if the current workspace is deleted, open the first workspace + if (state.currentWorkspace?.workspaceId == workspaceId) { + add(OpenWorkspace(state.workspaces.first.workspaceId)); + } + // remove the deleted workspace from the list instead of fetching + // the workspaces again + final workspaces = [...state.workspaces]..removeWhere( + (e) => e.workspaceId == workspaceId, + ); + return ( + workspaces, + FlowyResult.success(null) + ); + }, + (e) { + Log.error(e); + return (state.workspaces, FlowyResult.failure(e)); + }, + ); + emit( state.copyWith( openWorkspaceResult: null, createWorkspaceResult: null, - deleteWorkspaceResult: - result.fold((s) => FlowyResult.success(null), (e) { - Log.error(e); - return FlowyResult.failure(e); - }), + updateWorkspaceIconResult: null, + renameWorkspaceResult: null, + deleteWorkspaceResult: deleteWorkspaceResult, + workspaces: workspaces, ), ); }, openWorkspace: (workspaceId) async { - final result = await _userService.openWorkspace(workspaceId); + final (currentWorkspace, openWorkspaceResult) = + await _userService.openWorkspace(workspaceId).fold( + (s) { + final openedWorkspace = state.workspaces.firstWhere( + (e) => e.workspaceId == workspaceId, + ); + return ( + openedWorkspace, + FlowyResult.success(null) + ); + }, + (f) { + Log.error(f); + return (state.currentWorkspace, FlowyResult.failure(f)); + }, + ); + emit( state.copyWith( createWorkspaceResult: null, deleteWorkspaceResult: null, - openWorkspaceResult: - result.fold((s) => FlowyResult.success(null), (e) { - Log.error(e); - return FlowyResult.failure(e); - }), + updateWorkspaceIconResult: null, + openWorkspaceResult: openWorkspaceResult, + currentWorkspace: currentWorkspace, + ), + ); + }, + renameWorkspace: (workspaceId, name) async { + final result = await _userService.renameWorkspace( + workspaceId, + name, + ); + final (workspaces, currentWorkspace, renameWorkspaceResult) = + result.fold( + (s) { + final workspaces = state.workspaces.map((e) { + if (e.workspaceId == workspaceId) { + e.freeze(); + return e.rebuild((p0) { + p0.name = name; + }); + } + return e; + }).toList(); + + final currentWorkspace = workspaces.firstWhere( + (e) => e.workspaceId == state.currentWorkspace?.workspaceId, + ); + + return ( + workspaces, + currentWorkspace, + FlowyResult.success(null), + ); + }, + (e) { + Log.error(e); + return ( + state.workspaces, + state.currentWorkspace, + FlowyResult.failure(e), + ); + }, + ); + emit( + state.copyWith( + createWorkspaceResult: null, + deleteWorkspaceResult: null, + openWorkspaceResult: null, + updateWorkspaceIconResult: null, + workspaces: workspaces, + currentWorkspace: currentWorkspace, + renameWorkspaceResult: renameWorkspaceResult, + ), + ); + }, + updateWorkspaceIcon: (workspaceId, icon) async { + final result = await _userService.updateWorkspaceIcon( + workspaceId, + icon, + ); + + final (workspaces, currentWorkspace, updateWorkspaceIconResult) = + result.fold( + (s) { + final workspaces = state.workspaces.map((e) { + if (e.workspaceId == workspaceId) { + e.freeze(); + return e.rebuild((p0) { + // TODO(Lucas): the icon is not ready in the backend + }); + } + return e; + }).toList(); + + final currentWorkspace = workspaces.firstWhere( + (e) => e.workspaceId == state.currentWorkspace?.workspaceId, + ); + + return ( + workspaces, + currentWorkspace, + FlowyResult.success(null), + ); + }, + (e) { + Log.error(e); + return ( + state.workspaces, + state.currentWorkspace, + FlowyResult.failure(e), + ); + }, + ); + + emit( + state.copyWith( + createWorkspaceResult: null, + deleteWorkspaceResult: null, + openWorkspaceResult: null, + renameWorkspaceResult: null, + updateWorkspaceIconResult: updateWorkspaceIconResult, + workspaces: workspaces, + currentWorkspace: currentWorkspace, ), ); }, @@ -83,24 +250,17 @@ class UserWorkspaceBloc extends Bloc { Future<(UserWorkspacePB currentWorkspace, List workspaces)?> _fetchWorkspaces() async { - final result = await _userService.getCurrentWorkspace(); - return result.fold((currentWorkspace) async { - final result = await _userService.getWorkspaces(); - return result.fold((workspaces) { - return ( - workspaces.firstWhere( - (e) => e.workspaceId == currentWorkspace.id, - ), - workspaces - ); - }, (e) { - Log.error(e); - return null; - }); - }, (e) { + try { + final currentWorkspace = + await _userService.getCurrentWorkspace().getOrThrow(); + final workspaces = await _userService.getWorkspaces().getOrThrow(); + final currentWorkspaceInList = + workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id); + return (currentWorkspaceInList, workspaces); + } catch (e) { Log.error(e); return null; - }); + } } } @@ -114,6 +274,14 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent { DeleteWorkspace; const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = OpenWorkspace; + const factory UserWorkspaceEvent.renameWorkspace( + String workspaceId, + String name, + ) = _RenameWorkspace; + const factory UserWorkspaceEvent.updateWorkspaceIcon( + String workspaceId, + String icon, + ) = _UpdateWorkspaceIcon; const factory UserWorkspaceEvent.workspacesReceived( FlowyResult, FlowyError> workspacesOrFail, ) = WorkspacesReceived; @@ -127,8 +295,10 @@ class UserWorkspaceState with _$UserWorkspaceState { @Default(null) FlowyResult? createWorkspaceResult, @Default(null) FlowyResult? deleteWorkspaceResult, @Default(null) FlowyResult? openWorkspaceResult, + @Default(null) FlowyResult? renameWorkspaceResult, + @Default(null) FlowyResult? updateWorkspaceIconResult, }) = _UserWorkspaceState; - factory UserWorkspaceState.initial() => + factory UserWorkspaceState.initial() => const UserWorkspaceState(currentWorkspace: null, workspaces: []); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart index c8a44d2f75..ec86203599 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; @@ -125,8 +125,8 @@ class _PersonalFolderHeaderState extends State { LocaleKeys.newPageText.tr(), (viewName, _) { if (viewName.isNotEmpty) { - context.read().add( - MenuEvent.createApp( + context.read().add( + SidebarRootViewsEvent.createRootView( viewName, index: 0, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 8a284464b8..e02a5b0c74 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,14 +1,13 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; @@ -22,6 +21,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -81,44 +81,72 @@ class _HomeSideBarState extends State { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => getIt(), - ), - BlocProvider( - create: (_) => MenuBloc( - user: widget.userProfile, - workspaceId: widget.workspaceSetting.workspaceId, - )..add(const MenuEvent.initial()), - ), - ], - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedView?.id != c.lastCreatedView?.id, - listener: (context, state) => context.read().add( - TabsEvent.openPlugin(plugin: state.lastCreatedView!.plugin()), + return BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: widget.userProfile) + ..add(const UserWorkspaceEvent.fetchWorkspaces()), + child: BlocBuilder( + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + builder: (context, state) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => getIt(), + ), + BlocProvider( + create: (_) => SidebarRootViewsBloc() + ..add( + SidebarRootViewsEvent.initial( + widget.userProfile, + state.currentWorkspace?.workspaceId ?? + widget.workspaceSetting.workspaceId, + ), + ), + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) => context.read().add( + TabsEvent.openPlugin( + plugin: state.lastCreatedRootView!.plugin(), + ), + ), ), - ), - BlocListener( - listenWhen: (_, curr) => curr.action != null, - listener: _onNotificationAction, - ), - ], - child: Builder( - builder: (context) { - final menuState = context.watch().state; - final favoriteState = context.watch().state; + BlocListener( + listenWhen: (_, curr) => curr.action != null, + listener: _onNotificationAction, + ), + BlocListener( + listener: (context, state) { + context.read().add( + SidebarRootViewsEvent.reset( + widget.userProfile, + state.currentWorkspace?.workspaceId ?? + widget.workspaceSetting.workspaceId, + ), + ); + }, + ), + ], + child: Builder( + builder: (context) { + final menuState = context.watch().state; + final favoriteState = context.watch().state; - return _buildSidebar( - context, - menuState.views, - favoriteState.views, - ); - }, - ), + return _buildSidebar( + context, + menuState.views, + favoriteState.views, + ); + }, + ), + ), + ); + }, ), ); } @@ -195,8 +223,11 @@ class _HomeSideBarState extends State { final action = state.action; if (action != null) { if (action.type == ActionType.openView) { - final view = - context.read().state.views.findView(action.objectId); + final view = context + .read() + .state + .views + .findView(action.objectId); if (view != null) { final Map arguments = {}; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart index 909124f70a..d5cd8a65ae 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -25,7 +25,9 @@ class SidebarNewPageButton extends StatelessWidget { LocaleKeys.newPageText.tr(), (viewName, _) { if (viewName.isNotEmpty) { - context.read().add(MenuEvent.createApp(viewName)); + context + .read() + .add(SidebarRootViewsEvent.createRootView(viewName)); } }, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart index 1c7452584a..71c04cf048 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart @@ -4,7 +4,7 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -24,7 +24,7 @@ class SidebarTopMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return SizedBox( height: HomeSizes.topBarHeight, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart index c6fe4b4d5a..1ac1afbabd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart @@ -3,7 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -13,6 +13,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarWorkspace extends StatelessWidget { @@ -27,32 +28,28 @@ class SidebarWorkspace extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: userProfile) - ..add(const UserWorkspaceEvent.fetchWorkspaces()), - child: BlocConsumer( - listener: _showResultDialog, - builder: (context, state) { - final currentWorkspace = state.currentWorkspace; - // todo: show something if there is no workspace - if (currentWorkspace == null) { - return const SizedBox.shrink(); - } - return Row( - children: [ - Expanded( - child: _WorkspaceWrapper( - userProfile: userProfile, - currentWorkspace: currentWorkspace, - ), + return BlocConsumer( + listener: _showResultDialog, + builder: (context, state) { + final currentWorkspace = state.currentWorkspace; + // todo: show something if there is no workspace + if (currentWorkspace == null) { + return const SizedBox.shrink(); + } + return Row( + children: [ + Expanded( + child: _WorkspaceWrapper( + userProfile: userProfile, + currentWorkspace: currentWorkspace, ), - UserSettingButton(userProfile: userProfile), - const HSpace(4), - NotificationButton(views: views), - ], - ); - }, - ), + ), + UserSettingButton(userProfile: userProfile), + const HSpace(4), + NotificationButton(views: views), + ], + ); + }, ); } @@ -86,6 +83,26 @@ class SidebarWorkspace extends StatelessWidget { showSnackBarMessage(context, message); return; } + + result = state.updateWorkspaceIconResult; + if (result != null) { + final message = result.fold( + (s) => LocaleKeys.workspace_updateIconSuccess.tr(), + (e) => '${LocaleKeys.workspace_updateIconFailed.tr()}: ${e.msg}', + ); + showSnackBarMessage(context, message); + return; + } + + result = state.renameWorkspaceResult; + if (result != null) { + final message = result.fold( + (s) => LocaleKeys.workspace_renameSuccess.tr(), + (e) => '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}', + ); + showSnackBarMessage(context, message); + return; + } } } @@ -161,6 +178,7 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> { }, child: FlowyButton( onTap: () => controller.show(), + useIntrinsicWidth: true, margin: const EdgeInsets.symmetric(vertical: 8), text: Row( children: [ @@ -170,9 +188,11 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> { child: WorkspaceIcon(workspace: widget.currentWorkspace), ), const HSpace(8), - FlowyText.medium( - widget.currentWorkspace.name, - overflow: TextOverflow.ellipsis, + Expanded( + child: FlowyText.medium( + widget.currentWorkspace.name, + overflow: TextOverflow.ellipsis, + ), ), const FlowySvg(FlowySvgs.drop_menu_show_m), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index 84dd7abe52..7fa07bcfe5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -64,23 +64,34 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { ), margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), onTap: () async { + PopoverContainer.of(context).closeAll(); + + final workspaceBloc = context.read(); switch (inner) { case WorkspaceMoreAction.delete: await NavigatorAlertDialog( title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), confirm: () { - context.read().add( - UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), - ); + workspaceBloc.add( + UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), + ); }, ).show(context); case WorkspaceMoreAction.rename: - - // TODO(Lucas): integrate with the backend - } - - if (context.mounted) { - PopoverContainer.of(context).closeAll(); + await NavigatorTextFieldDialog( + title: LocaleKeys.workspace_create.tr(), + value: workspace.name, + hintText: '', + autoSelectAllText: true, + onConfirm: (name, context) async { + workspaceBloc.add( + UserWorkspaceEvent.renameWorkspace( + workspace.workspaceId, + name, + ), + ); + }, + ).show(context); } }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index fdf7935482..93d60414bd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,7 +1,11 @@ +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class WorkspaceIcon extends StatelessWidget { const WorkspaceIcon({ @@ -13,17 +17,37 @@ class WorkspaceIcon extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO(Lucas): support icon later - return Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: ColorGenerator.generateColorFromString(workspace.name), - borderRadius: BorderRadius.circular(4), - ), - child: FlowyText( - workspace.name.isEmpty ? '' : workspace.name.substring(0, 1), - fontSize: 16, - color: Colors.black, + return AppFlowyPopover( + offset: const Offset(0, 8), + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(360, 380)), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (BuildContext popoverContext) { + return FlowyIconPicker( + onSelected: (result) { + context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + workspace.workspaceId, + result.emoji, + ), + ); + }, + ); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: ColorGenerator.generateColorFromString(workspace.name), + borderRadius: BorderRadius.circular(4), + ), + child: FlowyText( + workspace.name.isEmpty ? '' : workspace.name.substring(0, 1), + fontSize: 16, + color: Colors.black, + ), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart similarity index 77% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 359933970a..c02b2ad22e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -4,10 +4,8 @@ import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -15,6 +13,9 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +@visibleForTesting +const createWorkspaceButtonKey = ValueKey('createWorkspaceButton'); + class WorkspacesMenu extends StatelessWidget { const WorkspacesMenu({ super.key, @@ -38,14 +39,17 @@ class WorkspacesMenu extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), child: Row( children: [ - FlowyText.medium( - _getUserInfo(), - fontSize: 12.0, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, + Expanded( + child: FlowyText.medium( + _getUserInfo(), + fontSize: 12.0, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).hintColor, + ), ), - const Spacer(), + const HSpace(4.0), FlowyButton( + key: createWorkspaceButtonKey, useIntrinsicWidth: true, text: const FlowySvg(FlowySvgs.add_m), onTap: () { @@ -57,7 +61,7 @@ class WorkspacesMenu extends StatelessWidget { ), ), for (final workspace in workspaces) ...[ - _WorkspaceMenuItem( + WorkspaceMenuItem( workspace: workspace, userProfile: userProfile, isSelected: workspace.workspaceId == currentWorkspace.workspaceId, @@ -82,29 +86,19 @@ class WorkspacesMenu extends StatelessWidget { Future _showCreateWorkspaceDialog(BuildContext context) async { if (context.mounted) { - await NavigatorTextFieldDialog( - title: LocaleKeys.workspace_create.tr(), - value: '', - hintText: '', - autoSelectAllText: true, - onConfirm: (name, context) async { - final request = CreateWorkspacePB.create()..name = name; - final result = await UserEventCreateWorkspace(request).send(); - final message = result.fold( - (s) => LocaleKeys.workspace_createSuccess.tr(), - (e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}', - ); - if (context.mounted) { - showSnackBarMessage(context, message); - } + final workspaceBloc = context.read(); + await CreateWorkspaceDialog( + onConfirm: (name) { + workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name, '')); }, ).show(context); } } } -class _WorkspaceMenuItem extends StatelessWidget { - const _WorkspaceMenuItem({ +class WorkspaceMenuItem extends StatelessWidget { + const WorkspaceMenuItem({ + super.key, required this.workspace, required this.userProfile, required this.isSelected, @@ -143,9 +137,10 @@ class _WorkspaceMenuItem extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), iconPadding: 10.0, leftIconSize: const Size.square(32), - leftIcon: WorkspaceIcon( - workspace: workspace, + leftIcon: const SizedBox.square( + dimension: 32, ), + rightIcon: const HSpace(42.0), text: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -163,6 +158,15 @@ class _WorkspaceMenuItem extends StatelessWidget { ], ), ), + Positioned( + left: 12, + child: SizedBox.square( + dimension: 32, + child: WorkspaceIcon( + workspace: workspace, + ), + ), + ), Positioned( right: 12.0, child: Align(child: _buildRightIcon(context)), @@ -187,9 +191,28 @@ class _WorkspaceMenuItem extends StatelessWidget { WorkspaceMoreActionList(workspace: workspace), const FlowySvg( FlowySvgs.blue_check_s, - blendMode: null, ), ], ); } } + +class CreateWorkspaceDialog extends StatelessWidget { + const CreateWorkspaceDialog({ + super.key, + required this.onConfirm, + }); + + final void Function(String name) onConfirm; + + @override + Widget build(BuildContext context) { + return NavigatorTextFieldDialog( + title: LocaleKeys.workspace_create.tr(), + value: '', + hintText: '', + autoSelectAllText: true, + onConfirm: (name, _) => onConfirm(name), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart index 1f19ffaa41..97b81cfe1a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart @@ -1,3 +1,4 @@ library appflowy_result; +export 'src/async_result.dart'; export 'src/result.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart new file mode 100644 index 0000000000..328aa03556 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart @@ -0,0 +1,33 @@ +import 'package:appflowy_result/appflowy_result.dart'; + +typedef FlowyAsyncResult = Future>; + +extension FlowyAsyncResultExtension + on FlowyAsyncResult { + Future getOrElse(S Function(F f) onFailure) { + return then((result) => result.getOrElse(onFailure)); + } + + Future getOrThrow() { + return then((result) => result.getOrThrow()); + } + + Future fold( + W Function(S s) onSuccess, + W Function(F f) onFailure, + ) { + return then((result) => result.fold(onSuccess, onFailure)); + } + + Future isError() { + return then((result) => result.isFailure()); + } + + Future isSuccess() { + return then((result) => result.isSuccess()); + } + + FlowyAsyncResult onFailure(void Function(F failure) onFailure) { + return then((result) => result..onFailure(onFailure)); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart index 88d3051332..dbffef42c7 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart @@ -1,30 +1,28 @@ -abstract class FlowyResult { +abstract class FlowyResult { const FlowyResult(); factory FlowyResult.success(S s) => FlowySuccess(s); - factory FlowyResult.failure(F e) => FlowyFailure(e); + factory FlowyResult.failure(F f) => FlowyFailure(f); - T fold(T Function(S s) onSuccess, T Function(F e) onFailure); + T fold(T Function(S s) onSuccess, T Function(F f) onFailure); FlowyResult map(T Function(S success) fn); - FlowyResult mapError(T Function(F error) fn); + FlowyResult mapError(T Function(F failure) fn); bool isSuccess(); bool isFailure(); S? toNullable(); - void onSuccess( - void Function(S s) onSuccess, - ); + void onSuccess(void Function(S s) onSuccess); + void onFailure(void Function(F f) onFailure); - void onFailure( - void Function(F f) onFailure, - ); + S getOrElse(S Function(F failure) onFailure); + S getOrThrow(); } -class FlowySuccess implements FlowyResult { +class FlowySuccess implements FlowyResult { final S _value; FlowySuccess(this._value); @@ -54,7 +52,7 @@ class FlowySuccess implements FlowyResult { } @override - FlowyResult mapError(T Function(F error) fn) { + FlowyResult mapError(T Function(F error) fn) { return FlowySuccess(_value); } @@ -80,40 +78,50 @@ class FlowySuccess implements FlowyResult { @override void onFailure(void Function(F failure) onFailure) {} + + @override + S getOrElse(S Function(F failure) onFailure) { + return _value; + } + + @override + S getOrThrow() { + return _value; + } } -class FlowyFailure implements FlowyResult { - final F _error; +class FlowyFailure implements FlowyResult { + final F _value; - FlowyFailure(this._error); + FlowyFailure(this._value); - F get error => _error; + F get error => _value; @override bool operator ==(Object other) => identical(this, other) || other is FlowyFailure && runtimeType == other.runtimeType && - _error == other._error; + _value == other._value; @override - int get hashCode => _error.hashCode; + int get hashCode => _value.hashCode; @override - String toString() => 'Failure(error: $_error)'; + String toString() => 'Failure(error: $_value)'; @override T fold(T Function(S s) onSuccess, T Function(F e) onFailure) => - onFailure(_error); + onFailure(_value); @override map(T Function(S success) fn) { - return FlowyFailure(_error); + return FlowyFailure(_value); } @override - FlowyResult mapError(T Function(F error) fn) { - return FlowyFailure(fn(_error)); + FlowyResult mapError(T Function(F error) fn) { + return FlowyFailure(fn(_value)); } @override @@ -136,6 +144,16 @@ class FlowyFailure implements FlowyResult { @override void onFailure(void Function(F failure) onFailure) { - onFailure(_error); + onFailure(_value); + } + + @override + S getOrElse(S Function(F failure) onFailure) { + return onFailure(_value); + } + + @override + S getOrThrow() { + throw _value; } } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart index cae6493ed4..7c2e115524 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../util.dart'; @@ -10,26 +10,33 @@ void main() { }); test('assert initial apps is the build-in app', () async { - final menuBloc = MenuBloc( - user: testContext.userProfile, - workspaceId: testContext.currentWorkspace.id, - )..add(const MenuEvent.initial()); + final menuBloc = SidebarRootViewsBloc() + ..add( + SidebarRootViewsEvent.initial( + testContext.userProfile, + testContext.currentWorkspace.id, + ), + ); + await blocResponseFuture(); assert(menuBloc.state.views.length == 1); }); test('reorder apps', () async { - final menuBloc = MenuBloc( - user: testContext.userProfile, - workspaceId: testContext.currentWorkspace.id, - )..add(const MenuEvent.initial()); + final menuBloc = SidebarRootViewsBloc() + ..add( + SidebarRootViewsEvent.initial( + testContext.userProfile, + testContext.currentWorkspace.id, + ), + ); await blocResponseFuture(); - menuBloc.add(const MenuEvent.createApp("App 1")); + menuBloc.add(const SidebarRootViewsEvent.createRootView("App 1")); await blocResponseFuture(); - menuBloc.add(const MenuEvent.createApp("App 2")); + menuBloc.add(const SidebarRootViewsEvent.createRootView("App 2")); await blocResponseFuture(); - menuBloc.add(const MenuEvent.createApp("App 3")); + menuBloc.add(const SidebarRootViewsEvent.createRootView("App 3")); await blocResponseFuture(); assert(menuBloc.state.views[1].name == 'App 1'); diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index ca2825a62a..2845f7e1ac 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -3817,6 +3817,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -4957,6 +4968,30 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + [[package]] name = "ring" version = "0.16.20" @@ -5897,6 +5932,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "regex", + "rfd", "semver", "serde", "serde_json", @@ -7052,6 +7088,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + [[package]] name = "windows" version = "0.39.0" @@ -7207,6 +7256,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + [[package]] name = "windows_aarch64_msvc" version = "0.39.0" @@ -7231,6 +7286,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + [[package]] name = "windows_i686_gnu" version = "0.39.0" @@ -7255,6 +7316,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + [[package]] name = "windows_i686_msvc" version = "0.39.0" @@ -7279,6 +7346,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + [[package]] name = "windows_x86_64_gnu" version = "0.39.0" @@ -7321,6 +7394,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + [[package]] name = "windows_x86_64_msvc" version = "0.39.0" diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 1d126a1cae..f94587fffc 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -34,7 +34,7 @@ lru = "0.12.0" [dependencies] serde_json.workspace = true serde.workspace = true -tauri = { version = "1.5", features = [ +tauri = { version = "1.5", features = [ "dialog-all", "clipboard-all", "fs-all", "shell-open", diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json index 8c8e36b10a..899bbeeb41 100644 --- a/frontend/appflowy_tauri/src-tauri/tauri.conf.json +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -20,8 +20,7 @@ "fs": { "all": true, "scope": [ - "$APPLOCALDATA/**", - "$APPLOCALDATA/images/*" + "$APPLOCALDATA/**" ], "readFile": true, "writeFile": true, @@ -37,6 +36,14 @@ "all": true, "writeText": true, "readText": true + }, + "dialog": { + "all": true, + "ask": true, + "confirm": true, + "message": true, + "open": true, + "save": true } }, "bundle": { diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts index 7e4ac9e636..bdc4b23600 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts @@ -2,7 +2,8 @@ import { Op } from 'quill-delta'; import { HTMLAttributes } from 'react'; import { Element } from 'slate'; import { ViewIconTypePB, ViewLayoutPB } from '@/services/backend'; -import { YXmlText } from 'yjs/dist/src/types/YXmlText'; +import { PageCover } from '$app_reducers/pages/slice'; +import * as Y from 'yjs'; export interface EditorNode { id: string; @@ -110,6 +111,7 @@ export interface MathEquationNode extends Element { } export enum ImageType { + Local = 0, Internal = 1, External = 2, } @@ -162,14 +164,22 @@ export interface MentionPage { } export interface EditorProps { - id: string; - sharedType?: YXmlText; title?: string; + cover?: PageCover; onTitleChange?: (title: string) => void; + onCoverChange?: (cover?: PageCover) => void; showTitle?: boolean; + id: string; disableFocus?: boolean; } +export interface LocalEditorProps { + disableFocus?: boolean; + sharedType: Y.XmlText; + id: string; + caretColor?: string; +} + export enum EditorNodeType { Text = 'text', Paragraph = 'paragraph', @@ -221,7 +231,9 @@ export enum MentionType { export interface Mention { // inline page ref id - page?: string; + page_id?: string; // reminder date ref id date?: string; + + type: MentionType; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg index 3e86e21b8d..0739605066 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg b/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg new file mode 100644 index 0000000000..aeaa6a0f29 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Colors.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Colors.tsx deleted file mode 100644 index af3a91b3b9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Colors.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export function Colors() { - return
; -} - -export default Colors; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx index 40a46fed81..84f73c8ebe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx @@ -1,13 +1,22 @@ import React, { useCallback, useState } from 'react'; import TextField from '@mui/material/TextField'; import { useTranslation } from 'react-i18next'; -import { pattern } from '$app/utils/open_url'; import Button from '@mui/material/Button'; -export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => void; onEscape?: () => void }) { +const urlPattern = /^https?:\/\/.+/; + +export function EmbedLink({ + onDone, + onEscape, + defaultLink, +}: { + defaultLink?: string; + onDone?: (value: string) => void; + onEscape?: () => void; +}) { const { t } = useTranslation(); - const [value, setValue] = useState(''); + const [value, setValue] = useState(defaultLink ?? ''); const [error, setError] = useState(false); const handleChange = useCallback( @@ -15,7 +24,7 @@ export function EmbedLink({ onDone, onEscape }: { onDone?: (value: string) => vo const value = e.target.value; setValue(value); - setError(!pattern.test(value)); + setError(!urlPattern.test(value)); }, [setValue, setError] ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx new file mode 100644 index 0000000000..bf29f68a2e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx @@ -0,0 +1,60 @@ +import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; +import { CircularProgress } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { ErrorOutline } from '@mui/icons-material'; + +export const LocalImage = forwardRef< + HTMLImageElement, + { + renderErrorNode?: () => React.ReactElement | null; + } & React.ImgHTMLAttributes +>((localImageProps, ref) => { + const { src, renderErrorNode, ...props } = localImageProps; + const imageRef = useRef(null); + const { t } = useTranslation(); + const [imageURL, setImageURL] = useState(''); + const [loading, setLoading] = useState(true); + const [isError, setIsError] = useState(false); + const loadLocalImage = useCallback(async () => { + if (!src) return; + setLoading(true); + setIsError(false); + const { readBinaryFile, BaseDirectory } = await import('@tauri-apps/api/fs'); + + try { + const buffer = await readBinaryFile(src, { dir: BaseDirectory.AppLocalData }); + const blob = new Blob([buffer]); + + setImageURL(URL.createObjectURL(blob)); + } catch (e) { + setIsError(true); + } + + setLoading(false); + }, [src]); + + useEffect(() => { + void loadLocalImage(); + }, [loadLocalImage]); + + if (loading) { + return ( +
+ + {t('editor.loading')}... +
+ ); + } + + if (isError) { + if (renderErrorNode) return renderErrorNode(); + return ( +
+ +
{t('editor.imageLoadFailed')}
+
+ ); + } + + return {'local; +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx index 7f3d828149..a6b66a4c1f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx @@ -1,7 +1,95 @@ -import React from 'react'; +import React, { useCallback } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import CloudUploadIcon from '@mui/icons-material/CloudUploadOutlined'; +import { notify } from '$app/components/_shared/notify'; +import { isTauri } from '$app/utils/env'; +import { getFileName, IMAGE_DIR, ALLOWED_IMAGE_EXTENSIONS, MAX_IMAGE_SIZE } from '$app/utils/upload_image'; -export function UploadImage() { - return
; +export function UploadImage({ onDone }: { onDone?: (url: string) => void }) { + const { t } = useTranslation(); + + const checkTauriFile = useCallback( + async (url: string) => { + const { readBinaryFile } = await import('@tauri-apps/api/fs'); + + const buffer = await readBinaryFile(url); + const blob = new Blob([buffer]); + + if (blob.size > MAX_IMAGE_SIZE) { + notify.error(t('document.imageBlock.error.invalidImageSize')); + return false; + } + + return true; + }, + [t] + ); + + const uploadTauriLocalImage = useCallback( + async (url: string) => { + const { copyFile, BaseDirectory, exists, createDir } = await import('@tauri-apps/api/fs'); + + const checked = await checkTauriFile(url); + + if (!checked) return; + + try { + const existDir = await exists(IMAGE_DIR, { dir: BaseDirectory.AppLocalData }); + + if (!existDir) { + await createDir(IMAGE_DIR, { dir: BaseDirectory.AppLocalData }); + } + + const filename = getFileName(url); + + await copyFile(url, `${IMAGE_DIR}/${filename}`, { dir: BaseDirectory.AppLocalData }); + const newUrl = `${IMAGE_DIR}/${filename}`; + + onDone?.(newUrl); + } catch (e) { + notify.error(t('document.plugins.image.imageUploadFailed')); + } + }, + [checkTauriFile, onDone, t] + ); + + const handleClickUpload = useCallback(async () => { + if (!isTauri()) return; + const { open } = await import('@tauri-apps/api/dialog'); + + const url = await open({ + multiple: false, + directory: false, + filters: [ + { + name: 'Image', + extensions: ALLOWED_IMAGE_EXTENSIONS, + }, + ], + }); + + if (!url || typeof url !== 'string') return; + + await uploadTauriLocalImage(url); + }, [uploadTauriLocalImage]); + + return ( +
+ +
+ ); } export default UploadImage; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx new file mode 100644 index 0000000000..fb65c709ce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx @@ -0,0 +1,128 @@ +import React, { SyntheticEvent, useCallback, useState } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs'; +import SwipeableViews from 'react-swipeable-views'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; + +export enum TAB_KEY { + Colors = 'colors', + UPLOAD = 'upload', + EMBED_LINK = 'embed_link', + UNSPLASH = 'unsplash', +} + +export type TabOption = { + key: TAB_KEY; + label: string; + Component: React.ComponentType<{ + onDone?: (value: string) => void; + onEscape?: () => void; + }>; + onDone?: (value: string) => void; +}; + +export function UploadTabs({ + tabOptions, + popoverProps, + containerStyle, + extra, +}: { + containerStyle?: React.CSSProperties; + tabOptions: TabOption[]; + popoverProps?: PopoverProps; + extra?: React.ReactNode; +}) { + const [tabValue, setTabValue] = useState(() => { + return tabOptions[0].key; + }); + + const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => { + setTabValue(newValue as TAB_KEY); + }, []); + + const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.stopPropagation(); + + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + popoverProps?.onClose?.({}, 'escapeKeyDown'); + } + + if (e.key === 'Tab') { + e.preventDefault(); + e.stopPropagation(); + setTabValue((prev) => { + const currentIndex = tabOptions.findIndex((tab) => tab.key === prev); + let nextIndex = currentIndex + 1; + + if (e.shiftKey) { + nextIndex = currentIndex - 1; + } + + return tabOptions[nextIndex % tabOptions.length]?.key ?? tabOptions[0].key; + }); + } + }, + [popoverProps, tabOptions] + ); + + return ( + +
+
+ + {tabOptions.map((tab) => { + const { key, label } = tab; + + return ; + })} + + {extra} +
+ +
+ + {tabOptions.map((tab, index) => { + const { key, Component, onDone } = tab; + + return ( + + popoverProps?.onClose?.({}, 'escapeKeyDown')} /> + + ); + })} + +
+
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts index f2eab1116b..28673cae5f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts @@ -1,4 +1,5 @@ export * from './Unsplash'; export * from './UploadImage'; export * from './EmbedLink'; -export * from './Colors'; +export * from './UploadTabs'; +export * from './LocalImage'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx index 99f444ac26..db179ffb7f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx @@ -1,33 +1,52 @@ import ViewIconGroup from '$app/components/_shared/view_title/ViewIconGroup'; -import { PageIcon } from '$app_reducers/pages/slice'; +import { PageCover, PageIcon } from '$app_reducers/pages/slice'; import ViewIcon from '$app/components/_shared/view_title/ViewIcon'; +import { ViewCover } from '$app/components/_shared/view_title/cover'; function ViewBanner({ icon, hover, onUpdateIcon, + showCover, + cover, + onUpdateCover, }: { icon?: PageIcon; hover: boolean; onUpdateIcon: (icon: string) => void; + showCover: boolean; + cover?: PageCover; + onUpdateCover?: (cover?: PageCover) => void; }) { return ( - <> -
- +
+ {showCover && cover && } + +
+
+ +
+
+ +
-
- -
- +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx index ec38130c05..66827bca56 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx @@ -29,7 +29,7 @@ function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon if (!icon) return null; return ( <> -
+
{icon.value}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx index 68377951e8..7c23e0587a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx @@ -1,31 +1,42 @@ import { useTranslation } from 'react-i18next'; -import { PageIcon } from '$app_reducers/pages/slice'; +import { CoverType, PageCover, PageIcon } from '$app_reducers/pages/slice'; import React, { useCallback } from 'react'; import { randomEmoji } from '$app/utils/emoji'; import { EmojiEmotionsOutlined } from '@mui/icons-material'; import Button from '@mui/material/Button'; +import { ReactComponent as ImageIcon } from '$app/assets/image.svg'; +import { ImageType } from '$app/application/document/document.types'; interface Props { icon?: PageIcon; - // onUpdateCover: (coverType: CoverType, cover: string) => void; onUpdateIcon: (icon: string) => void; + showCover: boolean; + cover?: PageCover; + onUpdateCover?: (cover: PageCover) => void; } -function ViewIconGroup({ icon, onUpdateIcon }: Props) { + +const defaultCover = { + cover_selection_type: CoverType.Asset, + cover_selection: 'app_flowy_abstract_cover_2.jpeg', + image_type: ImageType.Internal, +}; + +function ViewIconGroup({ icon, onUpdateIcon, showCover, cover, onUpdateCover }: Props) { const { t } = useTranslation(); const showAddIcon = !icon?.value; + const showAddCover = !cover && showCover; + const onAddIcon = useCallback(() => { const emoji = randomEmoji(); onUpdateIcon(emoji); }, [onUpdateIcon]); - // const onAddCover = useCallback(() => { - // const color = randomColor(); - // - // onUpdateCover(CoverType.Color, color); - // }, []); + const onAddCover = useCallback(() => { + onUpdateCover?.(defaultCover); + }, [onUpdateCover]); return (
@@ -34,11 +45,11 @@ function ViewIconGroup({ icon, onUpdateIcon }: Props) { {t('document.plugins.cover.addIcon')} )} - {/*{showAddCover && (*/} - {/* */} - {/*)}*/} + {showAddCover && ( + + )}
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx index 26f83ac921..8d81b6d4b7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import ViewBanner from '$app/components/_shared/view_title/ViewBanner'; -import { Page, PageIcon } from '$app_reducers/pages/slice'; +import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice'; import { ViewIconTypePB } from '@/services/backend'; import ViewTitleInput from '$app/components/_shared/view_title/ViewTitleInput'; @@ -9,9 +9,20 @@ interface Props { showTitle?: boolean; onTitleChange?: (title: string) => void; onUpdateIcon?: (icon: PageIcon) => void; + forceHover?: boolean; + showCover?: boolean; + onUpdateCover?: (cover?: PageCover) => void; } -function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpdateIconProp }: Props) { +function ViewTitle({ + view, + forceHover = false, + onTitleChange, + showTitle = true, + onUpdateIcon: onUpdateIconProp, + showCover = false, + onUpdateCover, +}: Props) { const [hover, setHover] = useState(false); const [icon, setIcon] = useState(view.icon); @@ -38,7 +49,14 @@ function ViewTitle({ view, onTitleChange, showTitle = true, onUpdateIcon: onUpda onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} > - + {showTitle && (
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx new file mode 100644 index 0000000000..78b8bbcc46 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { colorMap } from '$app/utils/color'; + +const colors = Object.entries(colorMap); + +function Colors({ onDone }: { onDone?: (value: string) => void }) { + return ( +
+ {colors.map(([name, value]) => ( +
onDone?.(name)} + /> + ))} +
+ ); +} + +export default Colors; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx new file mode 100644 index 0000000000..bd8c178380 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx @@ -0,0 +1,112 @@ +import React, { useMemo } from 'react'; +import { CoverType, PageCover } from '$app_reducers/pages/slice'; +import { PopoverOrigin } from '@mui/material/Popover'; +import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY, UploadImage } from '$app/components/_shared/image_upload'; +import { useTranslation } from 'react-i18next'; +import Colors from '$app/components/_shared/view_title/cover/Colors'; +import { ImageType } from '$app/application/document/document.types'; +import Button from '@mui/material/Button'; + +const initialOrigin: { + anchorOrigin: PopoverOrigin; + transformOrigin: PopoverOrigin; +} = { + anchorOrigin: { + vertical: 'bottom', + horizontal: 'center', + }, + transformOrigin: { + vertical: 'top', + horizontal: 'center', + }, +}; + +function CoverPopover({ + anchorEl, + open, + onClose, + onUpdateCover, + onRemoveCover, +}: { + anchorEl: HTMLElement | null; + open: boolean; + onClose: () => void; + onUpdateCover?: (cover?: PageCover) => void; + onRemoveCover?: () => void; +}) { + const { t } = useTranslation(); + const tabOptions: TabOption[] = useMemo(() => { + return [ + { + label: t('document.plugins.cover.colors'), + key: TAB_KEY.Colors, + Component: Colors, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Color, + cover_selection: value, + image_type: ImageType.Internal, + }); + }, + }, + { + label: t('button.upload'), + key: TAB_KEY.UPLOAD, + Component: UploadImage, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.Local, + }); + onClose(); + }, + }, + { + label: t('document.imageBlock.embedLink.label'), + key: TAB_KEY.EMBED_LINK, + Component: EmbedLink, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.External, + }); + onClose(); + }, + }, + { + key: TAB_KEY.UNSPLASH, + label: t('document.imageBlock.unsplash.label'), + Component: Unsplash, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.External, + }); + }, + }, + ]; + }, [onClose, onUpdateCover, t]); + + return ( + + {t('button.remove')} + + } + /> + ); +} + +export default CoverPopover; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx new file mode 100644 index 0000000000..f207e07886 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { CoverType, PageCover } from '$app_reducers/pages/slice'; +import { renderColor } from '$app/utils/color'; +import ViewCoverActions from '$app/components/_shared/view_title/cover/ViewCoverActions'; +import CoverPopover from '$app/components/_shared/view_title/cover/CoverPopover'; +import DefaultImage from '$app/assets/images/default_cover.jpg'; +import { ImageType } from '$app/application/document/document.types'; +import { LocalImage } from '$app/components/_shared/image_upload'; + +export function ViewCover({ cover, onUpdateCover }: { cover: PageCover; onUpdateCover?: (cover?: PageCover) => void }) { + const { + cover_selection_type: type, + cover_selection: value = '', + image_type: source, + } = useMemo(() => cover || {}, [cover]); + const [showAction, setShowAction] = useState(false); + const actionRef = useRef(null); + const [showPopover, setShowPopover] = useState(false); + + const renderCoverColor = useCallback((color: string) => { + return ( +
+ ); + }, []); + + const renderCoverImage = useCallback((url: string) => { + return {''}; + }, []); + + const handleRemoveCover = useCallback(() => { + onUpdateCover?.(null); + }, [onUpdateCover]); + + const handleClickChange = useCallback(() => { + setShowPopover(true); + }, []); + + return ( +
{ + setShowAction(true); + }} + onMouseLeave={() => { + setShowAction(false); + }} + className={'relative flex h-[255px] w-full'} + > + {source === ImageType.Local ? ( + + ) : ( + <> + {type === CoverType.Asset ? renderCoverImage(DefaultImage) : null} + {type === CoverType.Color ? renderCoverColor(value) : null} + {type === CoverType.Image ? renderCoverImage(value) : null} + + )} + + + {showPopover && ( + setShowPopover(false)} + anchorEl={actionRef.current} + onUpdateCover={onUpdateCover} + onRemoveCover={handleRemoveCover} + /> + )} +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx new file mode 100644 index 0000000000..97615804fb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx @@ -0,0 +1,42 @@ +import React, { forwardRef } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg'; + +function ViewCoverActions( + { show, onRemove, onClickChange }: { show: boolean; onRemove: () => void; onClickChange: () => void }, + ref: React.ForwardedRef +) { + const { t } = useTranslation(); + + return ( +
+
+ +
+
+ ); +} + +export default forwardRef(ViewCoverActions); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts new file mode 100644 index 0000000000..8df50bb41e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts @@ -0,0 +1 @@ +export * from './ViewCover'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx index eeb8b85904..079a6fd75f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx @@ -1,12 +1,14 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import Editor from '$app/components/editor/Editor'; import { DocumentHeader } from 'src/appflowy_app/components/document/document_header'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { updatePageName } from '$app_reducers/pages/async_actions'; +import { PageCover } from '$app_reducers/pages/slice'; export function Document({ id }: { id: string }) { const page = useAppSelector((state) => state.pages.pageMap[id]); + const [cover, setCover] = useState(undefined); const dispatch = useAppDispatch(); const onTitleChange = useCallback( @@ -21,12 +23,29 @@ export function Document({ id }: { id: string }) { [dispatch, id] ); + const view = useMemo(() => { + return { + ...page, + cover, + }; + }, [page, cover]); + + useEffect(() => { + return () => { + setCover(undefined); + }; + }, [id]); + if (!page) return null; return ( -
- - +
+ +
+
+ +
+
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx index a944547870..f6e8736c54 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx @@ -1,15 +1,18 @@ -import React, { memo, useCallback } from 'react'; -import { Page, PageIcon } from '$app_reducers/pages/slice'; +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice'; import ViewTitle from '$app/components/_shared/view_title/ViewTitle'; import { updatePageIcon } from '$app/application/folder/page.service'; interface DocumentHeaderProps { page: Page; + onUpdateCover: (cover?: PageCover) => void; } -export function DocumentHeader({ page }: DocumentHeaderProps) { +export function DocumentHeader({ page, onUpdateCover }: DocumentHeaderProps) { const pageId = page.id; + const ref = useRef(null); + const [forceHover, setForceHover] = useState(false); const onUpdateIcon = useCallback( async (icon: PageIcon) => { await updatePageIcon(pageId, icon.value ? icon : undefined); @@ -17,10 +20,39 @@ export function DocumentHeader({ page }: DocumentHeaderProps) { [pageId] ); + useEffect(() => { + const parent = ref.current?.parentElement; + + if (!parent) return; + + const documentDom = parent.querySelector('.appflowy-editor') as HTMLElement; + + if (!documentDom) return; + + const handleMouseMove = (e: MouseEvent) => { + const isMoveInTitle = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-title')); + const isMoveInHeader = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-header')); + + setForceHover(isMoveInTitle || isMoveInHeader); + }; + + documentDom.addEventListener('mousemove', handleMouseMove); + return () => { + documentDom.removeEventListener('mousemove', handleMouseMove); + }; + }, []); + if (!page) return null; return ( -
- +
+
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts index d5a0e0c976..04a2e7c0f1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts @@ -49,6 +49,12 @@ export function wrapFormula(editor: ReactEditor, formula?: string) { Transforms.insertNodes(editor, formulaElement, { select: true, }); + + const path = editor.selection?.anchor.path; + + if (path) { + editor.select(path); + } } export function unwrapFormula(editor: ReactEditor) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts index a8135da1cc..91e6ecd76e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts @@ -254,17 +254,23 @@ export const CustomEditor = { }, insertMention(editor: ReactEditor, mention: Mention) { - const mentionElement = { - type: EditorInlineNodeType.Mention, - children: [{ text: '@' }], - data: { - ...mention, + const mentionElement = [ + { + type: EditorInlineNodeType.Mention, + children: [{ text: '$' }], + data: { + ...mention, + }, }, - }; + ]; Transforms.insertNodes(editor, mentionElement, { select: true, }); + + editor.collapse({ + edge: 'end', + }); }, toggleTodo(editor: ReactEditor, node: TodoListNode) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx index 0966edf384..a20300bbc2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx @@ -9,10 +9,8 @@ export const Callout = memo(
-
-
+
+
{children}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx index 6d3a7cfd22..9d2b4fdac0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx @@ -24,9 +24,9 @@ export const ImageBlock = memo( onClick={() => { if (!selected) onFocusNode(); }} - className={`${className} image-block relative w-full cursor-pointer py-1`} + className={`${className} image-block relative w-full cursor-pointer py-1`} > -
+
{children}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx index 01e4df6c5c..ceab0c2d64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { ImageNode } from '$app/application/document/document.types'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ImageNode, ImageType } from '$app/application/document/document.types'; import { useTranslation } from 'react-i18next'; import { CircularProgress } from '@mui/material'; import { ErrorOutline } from '@mui/icons-material'; @@ -7,6 +7,7 @@ import ImageResizer from '$app/components/editor/components/blocks/image/ImageRe import { CustomEditor } from '$app/components/editor/command'; import { useSlateStatic } from 'slate-react'; import ImageActions from '$app/components/editor/components/blocks/image/ImageActions'; +import { LocalImage } from '$app/components/_shared/image_upload'; function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) { const [loading, setLoading] = useState(true); @@ -14,7 +15,7 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) const imgRef = useRef(null); const editor = useSlateStatic(); - const { url, width: imageWidth } = node.data; + const { url = '', width: imageWidth, image_type: source } = node.data; const { t } = useTranslation(); const blockId = node.blockId; @@ -35,56 +36,71 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) setInitialWidth(imgRef.current.offsetWidth); } }, [hasError, initialWidth, loading]); + const imageProps: React.ImgHTMLAttributes = useMemo(() => { + return { + style: { width: loading || hasError ? '0' : imageWidth ?? '100%', opacity: selected ? 0.8 : 1 }, + className: 'object-cover', + ref: imgRef, + src: url, + draggable: false, + onLoad: () => { + setHasError(false); + setLoading(false); + }, + onError: () => { + setHasError(true); + setLoading(false); + }, + }; + }, [url, imageWidth, loading, hasError, selected]); + + const renderErrorNode = useCallback(() => { + return ( +
+ +
{t('editor.imageLoadFailed')}
+
+ ); + }, [t]); + + if (!url) return null; return ( - <> -
{ - setShowActions(true); - }} - onMouseLeave={() => { - setShowActions(false); - }} - className={'relative'} - > - { - setHasError(false); - setLoading(false); - }} - onError={() => { +
{ + setShowActions(true); + }} + onMouseLeave={() => { + setShowActions(false); + }} + className={`relative min-h-[48px] ${hasError || (loading && source !== ImageType.Local) ? 'w-full' : ''}`} + > + {source === ImageType.Local ? ( + { setHasError(true); - setLoading(false); + return null; }} - src={url} - alt={`image-${blockId}`} - className={'object-cover'} - style={{ width: loading || hasError ? '0' : imageWidth ?? '100%', opacity: selected ? 0.8 : 1 }} + loading={'lazy'} /> - {initialWidth && } - {showActions && } -
+ ) : ( + {`image-${blockId}`} + )} - {loading && ( -
+ {initialWidth && } + {showActions && } + {hasError ? ( + renderErrorNode() + ) : loading && source !== ImageType.Local ? ( +
{t('editor.loading')}
- )} - {hasError && ( -
- -
{t('editor.imageLoadFailed')}
-
- )} - + ) : null} +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx index a2164202a6..ff241937de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx @@ -29,18 +29,16 @@ function ImageResizer({ width, onWidthChange }: { width: number; onWidthChange: const onResizeStart = useCallback( (e: React.MouseEvent) => { startX.current = e.clientX; + originalWidth.current = width; document.addEventListener('mousemove', onResize); document.addEventListener('mouseup', onResizeEnd); }, - [onResize, onResizeEnd] + [onResize, onResizeEnd, width] ); return (
{ - originalWidth.current = width; - }} style={{ right: '2px', }} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx index 1c46776063..0aff9fb0cc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx @@ -1,20 +1,13 @@ -import React, { useCallback, useMemo, SyntheticEvent, useState } from 'react'; -import Popover, { PopoverOrigin } from '@mui/material/Popover/Popover'; +import React, { useMemo } from 'react'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; -import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; -import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs'; + import { useTranslation } from 'react-i18next'; -import { EmbedLink, Unsplash } from '$app/components/_shared/image_upload'; -import SwipeableViews from 'react-swipeable-views'; +import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY, UploadImage } from '$app/components/_shared/image_upload'; import { CustomEditor } from '$app/components/editor/command'; import { useSlateStatic } from 'slate-react'; import { ImageNode, ImageType } from '$app/application/document/document.types'; -enum TAB_KEY { - UPLOAD = 'upload', - EMBED_LINK = 'embed_link', - UNSPLASH = 'unsplash', -} const initialOrigin: { transformOrigin: PopoverOrigin; anchorOrigin: PopoverOrigin; @@ -53,13 +46,20 @@ function UploadPopover({ open, }); - const tabOptions = useMemo(() => { + const tabOptions: TabOption[] = useMemo(() => { return [ - // { - // label: t('button.upload'), - // key: TAB_KEY.UPLOAD, - // Component: UploadImage, - // }, + { + label: t('button.upload'), + key: TAB_KEY.UPLOAD, + Component: UploadImage, + onDone: (link: string) => { + CustomEditor.setImageBlockData(editor, node, { + url: link, + image_type: ImageType.Local, + }); + onClose(); + }, + }, { label: t('document.imageBlock.embedLink.label'), key: TAB_KEY.EMBED_LINK, @@ -87,102 +87,25 @@ function UploadPopover({ ]; }, [editor, node, onClose, t]); - const [tabValue, setTabValue] = useState(tabOptions[0].key); - - const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => { - setTabValue(newValue as TAB_KEY); - }, []); - - const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue); - - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - e.stopPropagation(); - - if (e.key === 'Escape') { - e.preventDefault(); - e.stopPropagation(); - onClose(); - } - - if (e.key === 'Tab') { - e.preventDefault(); - e.stopPropagation(); - setTabValue((prev) => { - const currentIndex = tabOptions.findIndex((tab) => tab.key === prev); - const nextIndex = (currentIndex + 1) % tabOptions.length; - - return tabOptions[nextIndex]?.key ?? tabOptions[0].key; - }); - } - }, - [onClose, tabOptions] - ); - return ( - { - e.stopPropagation(); - }} - onKeyDown={onKeyDown} - PaperProps={{ - style: { - padding: 0, + { + e.stopPropagation(); }, }} - > -
- - {tabOptions.map((tab) => { - const { key, label } = tab; - - return ; - })} - - -
- - {tabOptions.map((tab, index) => { - const { key, Component, onDone } = tab; - - return ( - - - - ); - })} - -
-
-
+ containerStyle={{ + maxWidth: paperWidth, + maxHeight: paperHeight, + overflow: 'hidden', + }} + tabOptions={tabOptions} + /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx index 542eb977d9..ee441be624 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx @@ -54,7 +54,7 @@ export const MathEquation = memo( >
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx index f376f9cd75..6d04a77c2e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx @@ -4,7 +4,7 @@ import { EditorElementProps, PageNode } from '$app/application/document/document export const Page = memo( forwardRef>(({ node: _, children, ...attributes }, ref) => { const className = useMemo(() => { - return `${attributes.className ?? ''} pb-3 text-4xl font-bold`; + return `${attributes.className ?? ''} document-title pb-3 text-4xl font-bold`; }, [attributes.className]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx index b05000c6e2..83af8fdbd1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx @@ -5,62 +5,87 @@ import { EditorProps } from '$app/application/document/document.types'; import { Provider } from '$app/components/editor/provider'; import { YXmlText } from 'yjs/dist/src/types/YXmlText'; import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation'; +import isEqual from 'lodash-es/isEqual'; -export const CollaborativeEditor = memo(({ id, title, showTitle = true, onTitleChange, disableFocus }: EditorProps) => { - const [sharedType, setSharedType] = useState(null); - const provider = useMemo(() => { - setSharedType(null); - return new Provider(id, showTitle); - }, [id, showTitle]); +export const CollaborativeEditor = memo( + ({ id, title, cover, showTitle = true, onTitleChange, onCoverChange, ...props }: EditorProps) => { + const [sharedType, setSharedType] = useState(null); + const provider = useMemo(() => { + setSharedType(null); + return new Provider(id, showTitle); + }, [id, showTitle]); - const root = useMemo(() => { - if (!showTitle || !sharedType || !sharedType.doc) return null; + const root = useMemo(() => { + if (!showTitle || !sharedType || !sharedType.doc) return null; - return getYTarget(sharedType?.doc, [0]); - }, [sharedType, showTitle]); + return getYTarget(sharedType?.doc, [0]); + }, [sharedType, showTitle]); - const rootText = useMemo(() => { - if (!root) return null; - return getInsertTarget(root, [0]); - }, [root]); + const rootText = useMemo(() => { + if (!root) return null; + return getInsertTarget(root, [0]); + }, [root]); - useEffect(() => { - if (!rootText || rootText.toString() === title) return; + useEffect(() => { + if (!rootText || rootText.toString() === title) return; - if (rootText.length > 0) { - rootText.delete(0, rootText.length); + if (rootText.length > 0) { + rootText.delete(0, rootText.length); + } + + rootText.insert(0, title || ''); + }, [title, rootText]); + + useEffect(() => { + if (!root) return; + + const originalCover = root.getAttribute('data')?.cover; + + if (cover === undefined) return; + if (isEqual(originalCover, cover)) return; + root.setAttribute('data', { cover: cover ? cover : undefined }); + }, [cover, root]); + + useEffect(() => { + if (!root) return; + const rootId = root.getAttribute('blockId'); + + if (!rootId) return; + + const getCover = () => { + const data = root.getAttribute('data'); + + onCoverChange?.(data?.cover); + }; + + getCover(); + const onChange = () => { + onTitleChange?.(root.toString()); + getCover(); + }; + + root.observeDeep(onChange); + return () => root.unobserveDeep(onChange); + }, [onTitleChange, root, onCoverChange]); + + useEffect(() => { + provider.connect(); + const handleConnected = () => { + setSharedType(provider.sharedType); + }; + + provider.on('ready', handleConnected); + return () => { + setSharedType(null); + provider.off('ready', handleConnected); + provider.disconnect(); + }; + }, [provider]); + + if (!sharedType || id !== provider.id) { + return null; } - rootText.insert(0, title || ''); - }, [title, rootText]); - - useEffect(() => { - if (!root) return; - const onChange = () => { - onTitleChange?.(root.toString()); - }; - - root.observeDeep(onChange); - return () => root.unobserveDeep(onChange); - }, [onTitleChange, root]); - - useEffect(() => { - provider.connect(); - const handleConnected = () => { - setSharedType(provider.sharedType); - }; - - provider.on('ready', handleConnected); - return () => { - setSharedType(null); - provider.off('ready', handleConnected); - provider.disconnect(); - }; - }, [provider]); - - if (!sharedType || id !== provider.id) { - return null; + return ; } - - return ; -}); +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx index 3ac4f1e5c6..12b198b23e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx @@ -11,7 +11,6 @@ import { useShortcuts } from 'src/appflowy_app/components/editor/plugins/shortcu import { BlockActionsToolbar } from '$app/components/editor/components/tools/block_actions'; import { CircularProgress } from '@mui/material'; -import * as Y from 'yjs'; import { NodeEntry } from 'slate'; import { DecorateStateProvider, @@ -22,8 +21,9 @@ import { } from '$app/components/editor/stores'; import CommandPanel from '../tools/command_panel/CommandPanel'; import { EditorBlockStateProvider } from '$app/components/editor/stores/block'; +import { LocalEditorProps } from '$app/application/document/document.types'; -function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: string; disableFocus?: boolean }) { +function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: LocalEditorProps) { const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType); const decorateCodeHighlight = useDecorateCodeHighlight(editor); const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor); @@ -74,7 +74,10 @@ function Editor({ sharedType, disableFocus }: { sharedType: Y.XmlText; id: strin disableFocus={disableFocus} onKeyDown={onKeyDown} decorate={decorate} - className={'px-16 caret-text-title outline-none focus:outline-none'} + style={{ + caretColor, + }} + className={`px-16 outline-none focus:outline-none`} />
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx index 903ae5741f..1824d8a590 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx @@ -27,6 +27,7 @@ import { Text as TextComponent } from '../blocks/text'; import { Page } from '../blocks/page'; import { useElementState } from '$app/components/editor/components/editor/Element.hooks'; import UnSupportBlock from '$app/components/editor/components/blocks/_shared/unSupportBlock'; +import { renderColor } from '$app/utils/color'; function Element({ element, attributes, children }: RenderElementProps) { const node = element; @@ -98,8 +99,8 @@ function Element({ element, attributes, children }: RenderElementProps) { const data = (node.data as BlockData) || {}; return { - backgroundColor: data.bg_color, - color: data.font_color, + backgroundColor: data.bg_color ? renderColor(data.bg_color) : undefined, + color: data.font_color ? renderColor(data.font_color) : undefined, }; }, [node.data]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx index 468cc3d380..188ac33361 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx @@ -1,6 +1,7 @@ import React, { CSSProperties } from 'react'; import { RenderLeafProps } from 'slate-react'; import { Link } from '$app/components/editor/components/inline_nodes/link'; +import { renderColor } from '$app/utils/color'; export function Leaf({ attributes, children, leaf }: RenderLeafProps) { let newChildren = children; @@ -39,11 +40,11 @@ export function Leaf({ attributes, children, leaf }: RenderLeafProps) { const style: CSSProperties = {}; if (leaf.font_color) { - style['color'] = leaf.font_color.replace('0x', '#'); + style['color'] = renderColor(leaf.font_color); } if (leaf.bg_color) { - style['backgroundColor'] = leaf.bg_color.replace('0x', '#'); + style['backgroundColor'] = renderColor(leaf.bg_color); } if (leaf.href) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx index 47aada1143..fb32eb18a9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx @@ -3,10 +3,10 @@ import React from 'react'; // Put this at the start and end of an inline component to work around this Chromium bug: // https://bugs.chromium.org/p/chromium/issues/detail?id=1249405 -export const InlineChromiumBugfix = () => ( +export const InlineChromiumBugfix = ({ className }: { className?: string }) => ( { + if (selected && isCollapsed && !open) { + const afterPoint = editor.selection ? editor.after(editor.selection) : undefined; + + const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined; + + if (afterStart) { + editor.select(afterStart); + } + } + }, [editor, isCollapsed, selected, open]); + const handleClick = useCallback( (e: MouseEvent) => { const target = e.currentTarget; const path = getNodePath(editor, target); - ReactEditor.focus(editor); - Transforms.select(editor, path); - if (editor.selection) { - setRange(editor.selection); - openPopover(); - } + setRange(path); + openPopover(); }, [editor, openPopover, setRange] ); @@ -103,9 +113,9 @@ export const InlineFormula = memo( selected ? 'selected' : '' }`} > - + {children} - + {open && ( >(({ node, children, ...attributes }, ref) => { return ( - <> - - - {children} - - - + + + {children} + + + ); }) ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx index eba7c77169..12e5bab14d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx @@ -5,23 +5,44 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { pageTypeMap } from '$app_reducers/pages/slice'; import { getPage } from '$app/application/folder/page.service'; -import { useSelected } from 'slate-react'; +import { useSelected, useSlate } from 'slate-react'; import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg'; import { notify } from 'src/appflowy_app/components/_shared/notify'; import { subscribeNotifications } from '$app/application/notification'; import { FolderNotification } from '@/services/backend'; +import { Editor, Range } from 'slate'; -export function MentionLeaf({ children, mention }: { mention: Mention; children: React.ReactNode }) { +export function MentionLeaf({ mention }: { mention: Mention }) { const { t } = useTranslation(); const [page, setPage] = useState(null); const [error, setError] = useState(false); const navigate = useNavigate(); + const editor = useSlate(); const selected = useSelected(); + const isCollapsed = editor.selection && Range.isCollapsed(editor.selection); + + useEffect(() => { + if (selected && isCollapsed && page) { + const afterPoint = editor.selection ? editor.after(editor.selection) : undefined; + + const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined; + + if (afterStart) { + editor.select(afterStart); + } + } + }, [editor, isCollapsed, selected, page]); + const loadPage = useCallback(async () => { setError(true); - if (!mention.page) return; + // keep old field for backward compatibility + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const pageId = mention.page_id ?? mention.page; + + if (!pageId) return; try { - const page = await getPage(mention.page); + const page = await getPage(pageId); setPage(page); setError(false); @@ -29,7 +50,7 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children: setPage(null); setError(true); } - }, [mention.page]); + }, [mention]); useEffect(() => { void loadPage(); @@ -94,31 +115,27 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children: }, [page]); return ( - - - {page && ( + + {error ? ( + <> + + {t('document.mention.deleted')} + + ) : ( + page && ( <> - {page.icon?.value || } - {page.name || t('document.title.placeholder')} + {page.icon?.value || } + {page.name || t('document.title.placeholder')} - )} - {error && ( - <> - - - - {t('document.mention.deleted')} - - )} - - - {children} + ) + )} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx index 10b86e503a..b3bb665702 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx @@ -6,6 +6,7 @@ import KeyboardNavigation, { import { useTranslation } from 'react-i18next'; import { TitleOutlined } from '@mui/icons-material'; import { EditorMarkFormat } from '$app/application/document/document.types'; +import { ColorEnum, renderColor } from '$app/utils/color'; export interface ColorPickerProps { onChange?: (format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => void; @@ -39,8 +40,8 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro >
@@ -118,40 +119,40 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro content: renderColorItem(t('editor.backgroundColorDefault'), '', ''), }, { - key: `bg-gray-rgba(161,161,159,0.61)`, - content: renderColorItem(t('editor.backgroundColorGray'), '', 'rgba(161,161,159,0.61)'), + key: `bg-lime-${ColorEnum.Lime}`, + content: renderColorItem(t('editor.backgroundColorLime'), '', ColorEnum.Lime), }, { - key: `bg-brown-rgba(178,93,37,0.65)`, - content: renderColorItem(t('editor.backgroundColorBrown'), '', 'rgba(178,93,37,0.65)'), + key: `bg-aqua-${ColorEnum.Aqua}`, + content: renderColorItem(t('editor.backgroundColorAqua'), '', ColorEnum.Aqua), }, { - key: `bg-orange-rgba(248,156,71,0.65)`, - content: renderColorItem(t('editor.backgroundColorOrange'), '', 'rgba(248,156,71,0.65)'), + key: `bg-orange-${ColorEnum.Orange}`, + content: renderColorItem(t('editor.backgroundColorOrange'), '', ColorEnum.Orange), }, { - key: `bg-yellow-rgba(229,197,137,0.6)`, - content: renderColorItem(t('editor.backgroundColorYellow'), '', 'rgba(229,197,137,0.6)'), + key: `bg-yellow-${ColorEnum.Yellow}`, + content: renderColorItem(t('editor.backgroundColorYellow'), '', ColorEnum.Yellow), }, { - key: `bg-green-rgba(124,189,111,0.65)`, - content: renderColorItem(t('editor.backgroundColorGreen'), '', 'rgba(124,189,111,0.65)'), + key: `bg-green-${ColorEnum.Green}`, + content: renderColorItem(t('editor.backgroundColorGreen'), '', ColorEnum.Green), }, { - key: `bg-blue-rgba(100,174,199,0.71)`, - content: renderColorItem(t('editor.backgroundColorBlue'), '', 'rgba(100,174,199,0.71)'), + key: `bg-blue-${ColorEnum.Blue}`, + content: renderColorItem(t('editor.backgroundColorBlue'), '', ColorEnum.Blue), }, { - key: `bg-purple-rgba(182,114,234,0.63)`, - content: renderColorItem(t('editor.backgroundColorPurple'), '', 'rgba(182,114,234,0.63)'), + key: `bg-purple-${ColorEnum.Purple}`, + content: renderColorItem(t('editor.backgroundColorPurple'), '', ColorEnum.Purple), }, { - key: `bg-pink-rgba(238,142,179,0.6)`, - content: renderColorItem(t('editor.backgroundColorPink'), '', 'rgba(238,142,179,0.6)'), + key: `bg-pink-${ColorEnum.Pink}`, + content: renderColorItem(t('editor.backgroundColorPink'), '', ColorEnum.Pink), }, { - key: `bg-red-rgba(238,88,98,0.64)`, - content: renderColorItem(t('editor.backgroundColorRed'), '', 'rgba(238,88,98,0.64)'), + key: `bg-red-${ColorEnum.LightPink}`, + content: renderColorItem(t('editor.backgroundColorRed'), '', ColorEnum.LightPink), }, ], }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx index 20c326d640..ade9817503 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx @@ -26,6 +26,7 @@ export const canSetColorBlocks: EditorNodeType[] = [ EditorNodeType.NumberedListBlock, EditorNodeType.ToggleListBlock, EditorNodeType.QuoteBlock, + EditorNodeType.CalloutBlock, ]; export function BlockOperationMenu({ diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx index 76ba3d3404..806a2a3788 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx @@ -52,7 +52,8 @@ export function useMentionPanel({ closePanel(true); CustomEditor.insertMention(editor, { - page: id, + page_id: id, + type: MentionType.PageRef, }); }, [closePanel, editor] diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx index 1ebb783871..ddab776abc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx @@ -17,7 +17,6 @@ import { ReactComponent as GridIcon } from '$app/assets/grid.svg'; import { ReactComponent as ImageIcon } from '$app/assets/image.svg'; import { DataObjectOutlined, FunctionsOutlined, HorizontalRuleOutlined, MenuBookOutlined } from '@mui/icons-material'; import { CustomEditor } from '$app/components/editor/command'; -import { randomEmoji } from '$app/utils/emoji'; import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import { YjsEditor } from '@slate-yjs/core'; import { useEditorBlockDispatch } from '$app/components/editor/stores/block'; @@ -133,7 +132,7 @@ export function useSlashCommandPanel({ if (nodeType === EditorNodeType.CalloutBlock) { Object.assign(data, { - icon: randomEmoji(), + icon: '📌', }); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx index 38f18a1d30..c7bfc11352 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx @@ -27,6 +27,8 @@ export function Formula() { } requestAnimationFrame(() => { + const selection = editor.selection; + if (!selection) return; setRange(selection); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss index bd9ef76e9e..36adff3880 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss @@ -83,7 +83,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } -.text-content { +.text-content, [data-dark-mode="true"] .text-content { &.empty-content { @apply min-w-[1px]; span { @@ -94,7 +94,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } } -.text-element:has(.text-placeholder), .divider-node { +.text-element:has(.text-placeholder), .divider-node, [data-dark-mode="true"] .text-element:has(.text-placeholder), [data-dark-mode="true"] .divider-node { ::selection { @apply bg-transparent; } @@ -151,8 +151,19 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } } -.image-block, .math-equation-block { +.image-block, .math-equation-block, [data-dark-mode="true"] .image-block, [data-dark-mode="true"] .math-equation-block { ::selection { @apply bg-transparent; } + &:hover { + .container-bg { + background: var(--content-blue-100) !important; + } + } +} + +.mention-inline { + &:hover { + @apply bg-fill-list-active rounded; + } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts index c74f0e993b..ead982006c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts @@ -28,8 +28,11 @@ export class Provider extends EventEmitter { sharedType.applyDelta(delta); const rootId = this.dataClient.rootId as string; + const root = delta[0].insert as Y.XmlText; + const data = root.getAttribute('data'); sharedType.setAttribute('blockId', rootId); + sharedType.setAttribute('data', data); this.sharedType = sharedType; this.sharedType?.observeDeep(this.onChange); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts index 42d0f372c7..b4da4b3ca7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts @@ -7,76 +7,90 @@ export function generateId() { return nanoid(10); } -export function transformToInlineElement(op: Op): Element | null { +export function transformToInlineElement(op: Op): Element[] { const attributes = op.attributes; - if (!attributes) return null; + if (!attributes) return []; const { formula, mention, ...attrs } = attributes; if (formula) { - return { - type: EditorInlineNodeType.Formula, - data: formula, - children: [ - { - text: op.insert as string, - ...attrs, - }, - ], - }; + const texts = (op.insert as string).split(''); + + return texts.map((text) => { + return { + type: EditorInlineNodeType.Formula, + data: formula, + children: [ + { + text, + ...attrs, + }, + ], + }; + }); } if (mention) { - return { - type: EditorInlineNodeType.Mention, - children: [ - { - text: op.insert as string, - ...attrs, + const texts = (op.insert as string).split(''); + + return texts.map((text) => { + return { + type: EditorInlineNodeType.Mention, + children: [ + { + text, + ...attrs, + }, + ], + data: { + ...(mention as Mention), }, - ], - data: { - ...(mention as Mention), - }, - }; + }; + }); } - return null; + return []; } export function getInlinesWithDelta(delta?: Op[]): (Text | Element)[] { - return delta && delta.length > 0 - ? delta.map((op) => { - const matchInline = transformToInlineElement(op); + const newDelta: (Text | Element)[] = []; - if (matchInline) { - return matchInline; - } + if (!delta || !delta.length) + return [ + { + text: '', + }, + ]; - if (op.attributes) { - if ('font_color' in op.attributes && op.attributes['font_color'] === '') { - delete op.attributes['font_color']; - } + delta.forEach((op) => { + const matchInlines = transformToInlineElement(op); - if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') { - delete op.attributes['bg_color']; - } + if (matchInlines.length > 0) { + newDelta.push(...matchInlines); + return; + } - if ('code' in op.attributes && !op.attributes['code']) { - delete op.attributes['code']; - } - } + if (op.attributes) { + if ('font_color' in op.attributes && op.attributes['font_color'] === '') { + delete op.attributes['font_color']; + } - return { - text: op.insert as string, - ...op.attributes, - }; - }) - : [ - { - text: '', - }, - ]; + if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') { + delete op.attributes['bg_color']; + } + + if ('code' in op.attributes && !op.attributes['code']) { + delete op.attributes['code']; + } + } + + newDelta.push({ + text: op.insert as string, + ...op.attributes, + }); + }); + + return newDelta; } export function convertToSlateValue(data: EditorData, includeRoot: boolean): Element[] { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss index a708777326..e844bd6729 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss @@ -1,12 +1,31 @@ +* { + margin: 0; + padding: 0; +} + +.appflowy-scroll-container { + &::-webkit-scrollbar { + width: 0px; + } +} + .workspaces { ::-webkit-scrollbar { width: 0px; } } + + .MuiPopover-root, .MuiPaper-root { ::-webkit-scrollbar { width: 0; height: 0; } +} + +.view-icon { + &:hover { + background-color: rgba(156, 156, 156, 0.20); + } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts index 8d3f07507e..57fe941acc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -1,6 +1,8 @@ import { ViewIconTypePB, ViewLayoutPB, ViewPB } from '@/services/backend'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import isEqual from 'lodash-es/isEqual'; +import { ImageType } from '$app/application/document/document.types'; +import { Nullable } from 'unsplash-js/dist/helpers/typescript'; export const pageTypeMap = { [ViewLayoutPB.Document]: 'document', @@ -14,6 +16,7 @@ export interface Page { name: string; layout: ViewLayoutPB; icon?: PageIcon; + cover?: PageCover; } export interface PageIcon { @@ -21,6 +24,17 @@ export interface PageIcon { value: string; } +export enum CoverType { + Color = 'CoverType.color', + Image = 'CoverType.file', + Asset = 'CoverType.asset', +} +export type PageCover = Nullable<{ + image_type?: ImageType; + cover_selection_type?: CoverType; + cover_selection?: string; +}>; + export function parserViewPBToPage(view: ViewPB): Page { const icon = view.icon; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts new file mode 100644 index 0000000000..4861e4de2d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts @@ -0,0 +1,31 @@ +export enum ColorEnum { + Purple = 'appflowy_them_color_tint1', + Pink = 'appflowy_them_color_tint2', + LightPink = 'appflowy_them_color_tint3', + Orange = 'appflowy_them_color_tint4', + Yellow = 'appflowy_them_color_tint5', + Lime = 'appflowy_them_color_tint6', + Green = 'appflowy_them_color_tint7', + Aqua = 'appflowy_them_color_tint8', + Blue = 'appflowy_them_color_tint9', +} + +export const colorMap = { + [ColorEnum.Purple]: 'var(--tint-purple)', + [ColorEnum.Pink]: 'var(--tint-pink)', + [ColorEnum.LightPink]: 'var(--tint-red)', + [ColorEnum.Orange]: 'var(--tint-orange)', + [ColorEnum.Yellow]: 'var(--tint-yellow)', + [ColorEnum.Lime]: 'var(--tint-lime)', + [ColorEnum.Green]: 'var(--tint-green)', + [ColorEnum.Aqua]: 'var(--tint-aqua)', + [ColorEnum.Blue]: 'var(--tint-blue)', +}; + +export function renderColor(color: string) { + if (colorMap[color as ColorEnum]) { + return colorMap[color as ColorEnum]; + } + + return color.replace('0x', '#'); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts index fa2520bb7a..badd5c60a3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts @@ -39,12 +39,25 @@ export const getDesignTokens = (isDark: boolean): ThemeOptions => { styleOverrides: { contained: { color: 'var(--content-on-fill)', + boxShadow: 'var(--shadow)', }, containedPrimary: { '&:hover': { backgroundColor: 'var(--fill-default)', }, }, + containedInherit: { + color: 'var(--text-title)', + backgroundColor: isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.4)', + '&:hover': { + backgroundColor: 'var(--bg-body)', + boxShadow: 'var(--shadow)', + }, + }, + outlinedInherit: { + color: 'var(--text-title)', + borderColor: 'var(--line-divider)', + }, }, }, MuiButtonBase: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts new file mode 100644 index 0000000000..7d92eddd91 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts @@ -0,0 +1,9 @@ +export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB +export const ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png']; +export const IMAGE_DIR = 'images'; + +export function getFileName(url: string) { + const [...parts] = url.split('/'); + + return parts.pop() ?? url; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index cbd71ec7d4..03ba493c10 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -8,13 +8,7 @@ function DocumentPage() { const documentId = params.id; if (!documentId) return null; - return ( -
-
- -
-
- ); + return ; } export default DocumentPage; diff --git a/frontend/appflowy_tauri/src/styles/variables/light.variables.css b/frontend/appflowy_tauri/src/styles/variables/light.variables.css index b0ce72e1a5..cdaaf791a5 100644 --- a/frontend/appflowy_tauri/src/styles/variables/light.variables.css +++ b/frontend/appflowy_tauri/src/styles/variables/light.variables.css @@ -92,7 +92,7 @@ --fill-active: #e0f8ff; --fill-list-hover: #e0f8ff; --fill-list-active: #edeef2; - --content-blue-400: #00bcf0; + --content-blue-400: rgb(0, 188, 240); --content-blue-300: #52d1f4; --content-blue-600: #009fd1; --content-blue-100: #e0f8ff; @@ -111,7 +111,7 @@ --function-info: #00bcf0; --tint-purple: #e8e0ff; --tint-pink: #ffe7ee; - --tint-red: #ffe7ee; + --tint-red: #ffdddd; --tint-lime: #f5ffdc; --tint-green: #ddffd6; --tint-aqua: #defff1; diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/base.json b/frontend/appflowy_tauri/style-dictionary/tokens/base.json index 48ff92e680..4e31b0523d 100644 --- a/frontend/appflowy_tauri/style-dictionary/tokens/base.json +++ b/frontend/appflowy_tauri/style-dictionary/tokens/base.json @@ -134,7 +134,7 @@ "type": "color" }, "red": { - "value": "#ffe7ee", + "value": "#ffdddd", "type": "color" } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fdf804c38e..fc537dfb91 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -71,7 +71,11 @@ "deleteSuccess": "Workspace deleted successfully", "deleteFailed": "Failed to delete workspace", "openSuccess": "Open workspace successfully", - "openFailed": "Failed to open workspace" + "openFailed": "Failed to open workspace", + "renameSuccess": "Workspace renamed successfully", + "renameFailed": "Failed to rename workspace", + "updateIconSuccess": "Workspace reset successfully", + "updateIconFailed": "Failed to reset workspace" }, "shareAction": { "buttonText": "Share", @@ -911,8 +915,9 @@ "error": { "invalidImage": "Invalid image", "invalidImageSize": "Image size must be less than 5MB", - "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "Invalid image URL" + "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, JPG", + "invalidImageUrl": "Invalid image URL", + "noImage": "No such file or directory" }, "embedLink": { "label": "Embed link", @@ -1239,6 +1244,8 @@ "backgroundColorPurple": "Purple background", "backgroundColorPink": "Pink background", "backgroundColorRed": "Red background", + "backgroundColorLime": "Lime background", + "backgroundColorAqua": "Aqua background", "done": "Done", "cancel": "Cancel", "tint1": "Tint 1", diff --git a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh index f20f1b72c3..30538def96 100755 --- a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh +++ b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh @@ -1,13 +1,5 @@ #!/bin/bash -no_pub_get=false - -while getopts 's' flag; do - case "${flag}" in - s) no_pub_get=true ;; - esac -done - echo "Generating flowy icon files" # Store the current working directory diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 1cf5f0fe49..24c90650d2 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -1,13 +1,5 @@ #!/bin/bash -no_pub_get=false - -while getopts 's' flag; do - case "${flag}" in - s) no_pub_get=true ;; - esac -done - # Store the current working directory original_dir=$(pwd) @@ -19,9 +11,7 @@ cd ../../../appflowy_flutter # Navigate to the appflowy_flutter directory and generate files echo "Generating files for appflowy_flutter" -if [ "$no_pub_get" = false ]; then - flutter packages pub get >/dev/null 2>&1 -fi +flutter packages pub get >/dev/null 2>&1 dart run build_runner build -d echo "Done generating files for appflowy_flutter" @@ -36,9 +26,7 @@ for d in */; do if [ -f "pubspec.yaml" ]; then echo "Generating freezed files in $d..." echo "Please wait while we clean the project and fetch the dependencies." - if [ "$no_pub_get" = false ]; then - flutter packages pub get >/dev/null 2>&1 - fi + flutter packages pub get >/dev/null 2>&1 dart run build_runner build -d echo "Done running build command in $d" else diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.sh b/frontend/scripts/code_generation/language_files/generate_language_files.sh index 41abe4217a..8aa403d1f2 100755 --- a/frontend/scripts/code_generation/language_files/generate_language_files.sh +++ b/frontend/scripts/code_generation/language_files/generate_language_files.sh @@ -1,13 +1,5 @@ #!/bin/bash -no_pub_get=false - -while getopts 's' flag; do - case "${flag}" in - s) no_pub_get=true ;; - esac -done - echo "Generating language files" # Store the current working directory @@ -24,6 +16,9 @@ rm -rf assets/translations/ mkdir -p assets/translations/ cp -f ../resources/translations/*.json assets/translations/ +# the ci alwayas return a 'null check operator used on a null value' error. +# so we force to exec the below command to avoid the error. +# https://github.com/dart-lang/pub/issues/3314 flutter pub get flutter packages pub get diff --git a/frontend/scripts/makefile/flutter.toml b/frontend/scripts/makefile/flutter.toml index 6e6c8957c5..4203ce678d 100644 --- a/frontend/scripts/makefile/flutter.toml +++ b/frontend/scripts/makefile/flutter.toml @@ -82,10 +82,10 @@ run_task = { name = [ script_runner = "@shell" [tasks.appflowy-android-dev-ci] -dependencies = ["appflowy-core-dev-android"] +dependencies = ["appflowy-core-dev-android-ci"] run_task = { name = [ "code_generation", - "flutter-build-android", + "flutter-build-android-ci", ] } script_runner = "@shell" diff --git a/frontend/scripts/makefile/mobile.toml b/frontend/scripts/makefile/mobile.toml index 62214d7541..41d6888281 100644 --- a/frontend/scripts/makefile/mobile.toml +++ b/frontend/scripts/makefile/mobile.toml @@ -27,11 +27,11 @@ script = [ """ cd rust-lib/ rustup show - if [ "${BUILD_FLAG}" == "debug" ]; then - echo "🚀 🚀 🚀 Building for debug" + if [ "${BUILD_FLAG}" = "debug" ]; then + echo "🚀 🚀 🚀 Building iOS SDK for debug" cargo lipo --targets ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi else - echo "🚀 🚀 🚀 Building for release" + echo "🚀 🚀 🚀 Building iOS SDK for release" cargo lipo --release --targets ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi fi cd ../ @@ -49,18 +49,28 @@ run_task = { name = [ "restore-crate-type", ] } +# only use in CI job +[tasks.appflowy-core-dev-android-ci] +category = "Build" +dependencies = ["env_check", "set-app-version"] +run_task = { name = [ + "setup-crate-type", + "sdk-build-android-ci", + "post-mobile-android", + "restore-crate-type", +] } + [tasks.sdk-build-android] dependencies = ["set-app-version"] private = true script = [ """ cd rust-lib/ - rustup show if [ "${BUILD_FLAG}" = "debug" ]; then - echo "🚀 🚀 🚀 Building for debug" - cargo ndk -t arm64-v8a -t x86_64 -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi + echo "🚀 🚀 🚀 Building Android SDK for debug" + cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi else - echo "🚀 🚀 🚀 Building for release" + echo "🚀 🚀 🚀 Building Android SDK for release" cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi --release fi cd ../ @@ -68,6 +78,19 @@ script = [ ] script_runner = "@shell" +# only use in CI job +[tasks.sdk-build-android-ci] +dependencies = ["set-app-version"] +private = true +script = [ + """ + cd rust-lib/ + cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi + cd ../ + """, +] +script_runner = "@shell" + [tasks.post-mobile-ios] private = true script = [ @@ -76,6 +99,9 @@ script = [ dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/packages/appflowy_backend/${TARGET_OS} lib = set lib${LIB_NAME}.${LIB_EXT} + ls -a ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG} + + echo "💻 💻 💻 Copying ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} to ${dart_ffi_dir}/${lib}" rm -f ${dart_ffi_dir}/${lib} cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \