Compare commits

..
Author SHA1 Message Date
nathan c404ce5297 chore: clippy 2024-10-15 17:12:07 +08:00
nathan 64dfad284e chore: use same runtime 2024-10-06 22:55:50 +08:00
nathan ea34b226f1 chore: update calculation when filter change 2024-10-06 21:46:15 +08:00
nathan 3b696e4d27 Merge branch 'main' into fix_calculation_bug 2024-10-06 20:14:29 +08:00
nathan e714548dc3 chore: cal when open database 2024-10-03 22:51:40 +08:00
nathan 1ba339fabe chore: optimize calculate 2024-10-03 16:17:27 +08:00
nathan 90cf10e2c4 fix: calculate value 2024-10-03 13:27:35 +08:00
nathan bec1668272 chore: upgrade collab 2024-10-02 22:21:04 +08:00
578 changed files with 10397 additions and 25364 deletions

No files matched your search

@@ -52,7 +52,7 @@ runs:
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 keybinder-3.0 libnotify-dev network-manager
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager libmpv-dev mpv
shell: bash
- name: Enable Flutter Desktop
@@ -75,4 +75,4 @@ runs:
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test ${{ inputs.test_path }} -d Linux --coverage
shell: bash
shell: bash
+37 -44
View File
@@ -28,7 +28,6 @@ env:
FLUTTER_VERSION: "3.22.2"
RUST_TOOLCHAIN: "1.80.1"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.34-amd64
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -40,7 +39,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -74,9 +73,9 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [windows-latest]
os: [ windows-2019 ]
include:
- os: windows-latest
- os: windows-2019
flutter_profile: development-windows-x86
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
@@ -101,7 +100,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [macos-latest]
os: [ macos-latest ]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
@@ -123,12 +122,12 @@ jobs:
flutter_profile: ${{ matrix.flutter_profile }}
unit_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -174,7 +173,7 @@ jobs:
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 keybinder-3.0 libnotify-dev
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv
fi
shell: bash
@@ -217,11 +216,11 @@ jobs:
shell: bash
cloud_integration_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -248,9 +247,9 @@ jobs:
- name: Run Docker-Compose
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_CLOUD_VERSION: 0.6.29-amd64
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
@@ -293,7 +292,7 @@ jobs:
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 keybinder-3.0 libnotify-dev
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv
shell: bash
- name: Enable Flutter Desktop
@@ -326,12 +325,12 @@ jobs:
# split the integration tests into different machines to minimize the time
integration_test_1:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
@@ -340,6 +339,12 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install video dependency
run: |
sudo apt-get update
sudo apt-get -y install libmpv-dev mpv
shell: bash
- name: Flutter Integration Test 1
uses: ./.github/actions/flutter_integration_test
with:
@@ -350,12 +355,12 @@ jobs:
rust_target: ${{ matrix.target }}
integration_test_2:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
@@ -364,6 +369,12 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install video dependency
run: |
sudo apt-get update
sudo apt-get -y install libmpv-dev mpv
shell: bash
- name: Flutter Integration Test 2
uses: ./.github/actions/flutter_integration_test
with:
@@ -374,12 +385,12 @@ jobs:
rust_target: ${{ matrix.target }}
integration_test_3:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
@@ -388,6 +399,12 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install video dependency
run: |
sudo apt-get update
sudo apt-get -y install libmpv-dev mpv
shell: bash
- name: Flutter Integration Test 3
uses: ./.github/actions/flutter_integration_test
with:
@@ -396,27 +413,3 @@ jobs:
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
integration_test_4:
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Flutter Integration Test 4
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_4.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
+9 -9
View File
@@ -22,7 +22,6 @@ on:
env:
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.80.1"
CLOUD_VERSION: 0.6.34-amd64
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -49,13 +48,13 @@ jobs:
model: "iPhone 15"
shutdown_after_job: false
integration-tests:
build-macos:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: macos-latest
runs-on: macos-13
steps:
- name: Checkout source code
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -97,11 +96,11 @@ jobs:
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
- uses: futureware-tech/simulator-action@v3
id: simulator-action
with:
model: "iPhone 15"
shutdown_after_job: false
# - name: Run AppFlowy on simulator
# working-directory: frontend/appflowy_flutter
@@ -112,6 +111,7 @@ jobs:
# kill $pid
# continue-on-error: true
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
+1 -1
View File
@@ -368,7 +368,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install -y alien libnotify-dev
sudo apt-get install -y alien libnotify-dev libmpv-dev mpv
source $HOME/.cargo/env
cargo install --force cargo-make
cargo install --force duckscript_cli
+6 -12
View File
@@ -20,7 +20,6 @@ on:
env:
CARGO_TERM_COLOR: always
CLOUD_VERSION: 0.6.34-amd64
RUST_TOOLCHAIN: "1.77.2"
jobs:
@@ -47,9 +46,9 @@ jobs:
- name: Ensure AppFlowy-Cloud is Running with Correct Version
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
@@ -94,11 +93,6 @@ jobs:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Set timezone for action
uses: szenius/set-timezone@v2.0
with:
timezoneLinux: "US/Pacific"
- name: Maximize build space
run: |
sudo rm -rf /usr/share/dotnet
@@ -141,9 +135,9 @@ jobs:
- name: Ensure AppFlowy-Cloud is Running with Correct Version
working-directory: AppFlowy-Cloud
env:
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
+1 -34
View File
@@ -1,37 +1,4 @@
# Release Notes
## Version 0.7.2 - 22/10/2024
### New Features
- Copy link to block
- Support turn into in document
- Enable sharing links and publishing pages on mobile
- Enable drag and drop in row documents
- Right-click on page in sidebar to open more actions
- Create new subpage in document using `+` character
- Allow reordering checklist item
### Bug Fixes
- Fixed issue with inability to cancel inline code format in French IME
- Fixed delete with Shift or Ctrl shortcuts not working in documents
- Fixed the issues with incorrect time zone being used in filters.
## Version 0.7.1 - 07/10/2024
### New Features
- Copy link to share and open it in a browser
- Enable the ability to edit the page title within the body of the document
- Filter by last modified, created at, or a date range
- Allow customization of database property icons
- Support CTRL/CMD+X to delete the current line when the selection is collapsed in the document
- Support window tiling on macOS
- Add filters to grid views on mobile
- Create and manage workspaces on mobile
- Automatically convert property types for imported CSV files
### Bug Fixes
- Fixed calculations with filters applied
- Fixed issues with importing data folders into a cloud account
- Fixed French IME backtick issues
- Fixed selection gesture bugs on mobile
## Version 0.7.0 - 19/09/2024
### New Features
- Support reordering blocks in document with drag and drop
@@ -75,7 +42,7 @@
- Fixed the inability to edit group names on Kanban boards
- Made error codes more user-friendly
- Added leading zeros to day and month in date format
## Version 0.6.8 - 22/08/2024
### New Features
- Enabled viewing data inside a database record on mobile.
+1 -1
View File
@@ -119,7 +119,7 @@ We do not claim to outperform Notion in terms of functionality and design, at le
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information.
## Acknowledgments
## Acknowledgements
Special thanks to these amazing projects which help power AppFlowy.IO:
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.7.2"
APPFLOWY_VERSION = "0.7.1"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -1,3 +1,13 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
@@ -6,6 +16,17 @@ analyzer:
- "**/*.freezed.dart"
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
- require_trailing_commas
@@ -30,5 +51,8 @@ linter:
- sort_constructors_first
- unawaited_futures
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
errors:
invalid_annotation_target: ignore
@@ -3,13 +3,9 @@ import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'uncategorized/uncategorized_test_runner.dart'
as uncategorized_test_runner;
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
import 'data_migration/data_migration_test_runner.dart' as data_migration_test_runner;
Future<void> main() async {
preset_af_cloud_env_test.main();
data_migration_test_runner.main();
// uncategorized
uncategorized_test_runner.main();
@@ -22,4 +18,5 @@ Future<void> main() async {
// sidebar
sidebar_move_page_test.main();
}
@@ -1,275 +0,0 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// copy link to block
group('copy link to block:', () {
testWidgets('copy link to check if the clipboard has the correct content',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
await tester.editor.copyLinkToBlock([0]);
await tester.pumpAndSettle(Durations.short1);
// check the clipboard
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text,
matches(appflowySharePageLinkPattern),
);
});
testWidgets('copy link to block(another page) and paste it in doc',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
await tester.editor.copyLinkToBlock([0]);
// create a new page and paste it
const pageName = 'copy link to block';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// paste the link to the new page
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.paste();
await tester.pumpAndSettle();
// check the content of the block
final node = tester.editor.getNodeAtPath([0]);
final delta = node.delta!;
final insert = (delta.first as TextInsert).text;
final attributes = delta.first.attributes;
expect(insert, MentionBlockKeys.mentionChar);
final mention =
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
expect(mention[MentionBlockKeys.type], MentionType.page.name);
expect(mention[MentionBlockKeys.blockId], isNotNull);
expect(mention[MentionBlockKeys.pageId], isNotNull);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
Constants.gettingStartedPageName,
findRichText: true,
),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
// the pasted block content is 'Welcome to AppFlowy'
'Welcome to AppFlowy',
findRichText: true,
),
),
findsOneWidget,
);
// tap the mention block to jump to the page
await tester.tapButton(find.byType(MentionPageBlock));
await tester.pumpAndSettle();
// expect to go to the getting started page
final documentPage = find.byType(DocumentPage);
expect(documentPage, findsOneWidget);
expect(
tester.widget<DocumentPage>(documentPage).view.name,
Constants.gettingStartedPageName,
);
// and the block is selected
expect(
tester.widget<DocumentPage>(documentPage).initialBlockId,
mention[MentionBlockKeys.blockId],
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection.collapsed(
Position(
path: [0],
),
),
);
});
testWidgets('copy link to block(same page) and paste it in doc',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// create a new page and paste it
const pageName = 'copy link to block';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// copy the link to block from the first line
const inputText = 'Hello World';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(inputText);
await tester.ime.insertCharacter('\n');
await tester.pumpAndSettle();
await tester.editor.copyLinkToBlock([0]);
// paste the link to the second line
await tester.editor.tapLineOfEditorAt(1);
await tester.editor.paste();
await tester.pumpAndSettle();
// check the content of the block
final node = tester.editor.getNodeAtPath([1]);
final delta = node.delta!;
final insert = (delta.first as TextInsert).text;
final attributes = delta.first.attributes;
expect(insert, MentionBlockKeys.mentionChar);
final mention =
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
expect(mention[MentionBlockKeys.type], MentionType.page.name);
expect(mention[MentionBlockKeys.blockId], isNotNull);
expect(mention[MentionBlockKeys.pageId], isNotNull);
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
inputText,
findRichText: true,
),
),
findsNWidgets(2),
);
// edit the pasted block
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('!');
await tester.pumpAndSettle();
// check the content of the block
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.textContaining(
'$inputText!',
findRichText: true,
),
),
findsNWidgets(2),
);
// tap the mention block
await tester.tapButton(find.byType(MentionPageBlock));
expect(
tester.editor.getCurrentEditorState().selection,
Selection.collapsed(
Position(
path: [0],
),
),
);
});
testWidgets('''1. copy link to block from another page
2. paste the link to the new page
3. delete the original page
4. check the content of the block, it should be no access to the page
''', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open getting started page
await tester.openPage(Constants.gettingStartedPageName);
await tester.editor.copyLinkToBlock([0]);
// create a new page and paste it
const pageName = 'copy link to block';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// paste the link to the new page
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.paste();
await tester.pumpAndSettle();
// tap the mention block to jump to the page
await tester.tapButton(find.byType(MentionPageBlock));
await tester.pumpAndSettle();
// expect to go to the getting started page
final documentPage = find.byType(DocumentPage);
expect(documentPage, findsOneWidget);
expect(
tester.widget<DocumentPage>(documentPage).view.name,
Constants.gettingStartedPageName,
);
// delete the getting started page
await tester.hoverOnPageName(
Constants.gettingStartedPageName,
onHover: () async => tester.tapDeletePageButton(),
);
tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStarted);
// delete the page permanently
await tester.tapDeletePermanentlyButton();
// go back the page
await tester.openPage(pageName);
await tester.pumpAndSettle();
// check the content of the block
// it should be no access to the page
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.findTextInFlowyText(
LocaleKeys.document_mention_noAccess.tr(),
),
),
findsOneWidget,
);
});
});
}
@@ -68,6 +68,38 @@ void main() {
expect(afterMoveBlock.delta, beforeMoveBlock.delta);
});
// Copy link to block feature is disable temporarily, enable this test when the feature is ready.
// testWidgets('copy block link', (tester) async {
// await tester.initializeAppFlowy(
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
// );
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
// // open getting started page
// await tester.openPage(Constants.gettingStartedPageName);
// // hover and click on the option menu button beside the block component.
// await tester.editor.hoverAndClickOptionMenuButton([0]);
// // click the copy link to block option
// await tester.tap(
// find.findTextInFlowyText(
// LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),
// ),
// );
// await tester.pumpAndSettle(Durations.short1);
// // check the clipboard
// final content = await Clipboard.getData(Clipboard.kTextPlain);
// expect(
// content?.text,
// matches(
// r'^https:\/\/appflowy\.com\/app\/[a-f0-9-]{36}\/[a-f0-9-]{36}\?blockId=[A-Za-z0-9_-]+$',
// ),
// );
// });
testWidgets('hover on the block and delete it', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
@@ -1,12 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'document_copy_link_to_block_test.dart'
as document_copy_link_to_block_test;
import 'document_option_actions_test.dart' as document_option_actions_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
document_option_actions_test.main();
document_copy_link_to_block_test.main();
}
@@ -18,7 +18,6 @@ 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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -93,14 +92,6 @@ void main() {
await tester.pumpAndSettle();
}
// Attempt right-click on the page name and expect not to see
await tester.tap(gettingStarted.last, buttons: kSecondaryButton);
await tester.pumpAndSettle();
expect(
find.text(LocaleKeys.disclosureAction_moveTo.tr()),
findsOneWidget,
);
// move the current page to Getting started
await tester.tapButton(
gettingStarted.last,
@@ -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 '../../shared/util.dart';
import '../../../shared/util.dart';
// This test is meaningless, just for preventing the CI from failing.
void main() {
@@ -1,7 +1,9 @@
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
import 'user_setting_sync_test.dart' as user_sync_test;
void main() async {
preset_af_cloud_env_test.main();
appflowy_cloud_auth_test.main();
user_sync_test.main();
}
@@ -1,68 +0,0 @@
// 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/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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 'package:universal_platform/universal_platform.dart';
import '../../../shared/constants.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';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('workspace icon:', () {
testWidgets('remove icon from workspace', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openWorkspaceMenu();
// click the workspace icon
await tester.tapButton(
find.descendant(
of: find.byType(WorkspaceMenuItem),
matching: find.byType(WorkspaceIcon),
),
);
// click the remove icon button
await tester.tapButton(
find.text(LocaleKeys.button_remove.tr()),
);
// nothing should happen
expect(
find.text(LocaleKeys.workspace_updateIconSuccess.tr()),
findsNothing,
);
});
});
}
@@ -3,7 +3,6 @@ import 'package:integration_test/integration_test.dart';
import 'change_name_and_icon_test.dart' as change_name_and_icon_test;
import 'collaborative_workspace_test.dart' as collaborative_workspace_test;
import 'share_menu_test.dart' as share_menu_test;
import 'workspace_icon_test.dart' as workspace_icon_test;
import 'workspace_settings_test.dart' as workspace_settings_test;
void main() {
@@ -13,5 +12,4 @@ void main() {
share_menu_test.main();
collaborative_workspace_test.main();
change_name_and_icon_test.main();
workspace_icon_test.main();
}
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -277,72 +277,5 @@ void main() {
tester.assertRowDetailPageOpened();
});
testWidgets('filter calendar events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// Create the calendar view
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Calendar,
);
// Create a new event on the first of this month
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissEventEditor();
tester.assertNumberOfEventsInCalendar(1);
await tester.openCalendarEvent(index: 0, date: firstOfThisMonth);
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
await tester.createOption(name: "asdf");
await tester.createOption(name: "qwer");
await tester.selectOption(name: "asdf");
await tester.dismissCellEditor();
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, "Tags");
await tester.tapFilterButtonInGrid('Tags');
await tester.tapOptionFilterWithName('asdf');
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(0);
await tester.tapFilterButtonInGrid('Tags');
await tester.tapOptionFilterWithName('asdf');
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(1);
await tester.tapFilterButtonInGrid('Tags');
await tester.tapOptionFilterWithName('asdf');
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(0);
final secondOfThisMonth = DateTime(today.year, today.month, 2);
await tester.doubleClickCalendarCell(secondOfThisMonth);
await tester.dismissEventEditor();
tester.assertNumberOfEventsInCalendar(1);
await tester.openCalendarEvent(index: 0, date: secondOfThisMonth);
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
await tester.selectOption(name: "asdf");
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(0);
await tester.tapFilterButtonInGrid('Tags');
await tester.changeSelectFilterCondition(
SelectOptionFilterConditionPB.OptionIsEmpty,
);
await tester.dismissCellEditor();
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, secondOfThisMonth);
});
});
}
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -317,30 +316,6 @@ void main() {
);
});
testWidgets('text in viewport while typing', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.changeCalculateAtIndex(0, CalculationType.Count);
// add very large text with 200 lines
final largeText = List.generate(
200,
(index) => 'Line ${index + 1}',
).join('\n');
await tester.editCell(
rowIndex: 2,
fieldType: FieldType.RichText,
input: largeText,
);
// checks if last line is in view port
tester.expectToSeeText('Line 200');
});
// Disable this test because it fails on CI randomly
// testWidgets('last modified and created at field type options',
// (tester) async {
@@ -1,7 +1,7 @@
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -108,7 +108,7 @@ void main() {
await tester.tapOptionFilterWithName('s4');
// The row with 's4' should be shown.
tester.assertNumberOfRowsInGridPage(2);
tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
@@ -154,10 +154,12 @@ void main() {
tester.assertNumberOfRowsInGridPage(0);
await tester.tapFilterButtonInGrid('date');
await tester.changeDateFilterCondition(DateTimeFilterCondition.before);
await tester.tapDateFilterButtonInGrid();
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
tester.assertNumberOfRowsInGridPage(7);
await tester.changeDateFilterCondition(DateTimeFilterCondition.isEmpty);
await tester.tapDateFilterButtonInGrid();
await tester.tapDateFilterCondition(DateTimeFilterCondition.isEmpty);
tester.assertNumberOfRowsInGridPage(3);
await tester.pumpAndSettle();
@@ -169,6 +171,8 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
await tester.createField(
FieldType.CreatedTime,
name: 'Created at',
@@ -185,40 +189,11 @@ void main() {
tester.assertNumberOfRowsInGridPage(3);
await tester.tapFilterButtonInGrid('Created at');
await tester.changeDateFilterCondition(DateTimeFilterCondition.before);
await tester.tapDateFilterButtonInGrid();
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
tester.assertNumberOfRowsInGridPage(0);
await tester.pumpAndSettle();
});
testWidgets('create new row when filters don\'t autofill', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// create a filter
await tester.tapDatabaseFilterButton();
await tester.tapCreateFilterByFieldType(
FieldType.RichText,
'Name',
);
tester.assertNumberOfRowsInGridPage(3);
await tester.tapCreateRowButtonInGrid();
tester.assertNumberOfRowsInGridPage(4);
await tester.tapFilterButtonInGrid('Name');
await tester
.changeTextFilterCondition(TextFilterConditionPB.TextIsNotEmpty);
await tester.dismissCellEditor();
tester.assertNumberOfRowsInGridPage(0);
await tester.tapCreateRowButtonInGrid();
tester.assertNumberOfRowsInGridPage(0);
expect(find.byType(RowDetailPage), findsOneWidget);
await tester.pumpAndSettle();
});
});
}
@@ -1,6 +1,3 @@
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -12,7 +9,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -338,139 +335,5 @@ void main() {
tester.assertNumberOfRowsInGridPage(4);
});
testWidgets('edit checklist cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.Checklist;
await tester.createField(fieldType);
await tester.openFirstRowDetailPage();
await tester.hoverOnWidget(
find.byType(ChecklistRowDetailCell),
onHover: () async {
await tester.tapButton(find.byType(ChecklistItemControl));
},
);
tester.assertPhantomChecklistItemAtIndex(index: 0);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 1');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 1);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 2);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(find.byType(PhantomChecklistItem), findsNothing);
await tester.renameChecklistTask(index: 0, name: "task -1", enter: false);
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
tester.assertChecklistTaskInEditor(
index: 0,
name: "task -1",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 1);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 0');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertPhantomChecklistItemAtIndex(index: 2);
await tester.checkChecklistTask(index: 1);
expect(find.byType(PhantomChecklistItem), findsNothing);
expect(find.byType(ChecklistItem), findsNWidgets(3));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task -1",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 0",
isChecked: true,
);
tester.assertChecklistTaskInEditor(
index: 2,
name: "task 2",
isChecked: false,
);
await tester.tapButton(
find.descendant(
of: find.byType(ProgressAndHideCompleteButton),
matching: find.byType(FlowyIconButton),
),
);
expect(find.byType(ChecklistItem), findsNWidgets(2));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task -1",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
await tester.renameChecklistTask(index: 1, name: "task 3", enter: false);
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.renameChecklistTask(index: 0, name: "task 1", enter: false);
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
tester.assertChecklistTaskInEditor(
index: 2,
name: "task 3",
isChecked: false,
);
tester.assertPhantomChecklistItemAtIndex(index: 2);
await tester.checkChecklistTask(index: 1);
expect(find.byType(ChecklistItem), findsNWidgets(2));
});
});
}
@@ -1,84 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
import 'document_inline_page_reference_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Document deletion', () {
testWidgets('Trash breadcrumb', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// This test shares behavior with the inline page reference test, thus
// we utilize the same helper functions there.
final name = await createDocumentToReference(tester);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await triggerReferenceDocumentBySlashMenu(tester);
// Search for prefix of document
await enterDocumentText(tester);
// Select result
final optionFinder = find.descendant(
of: find.byType(InlineActionsHandler),
matching: find.text(name),
);
await tester.tap(optionFinder);
await tester.pumpAndSettle();
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
// Delete the page
await tester.hoverOnPageName(
name,
onHover: () async => tester.tapDeletePageButton(),
);
await tester.pumpAndSettle();
// Navigate to the deleted page from the inline mention
await tester.tap(mentionBlock);
await tester.pumpUntilFound(find.byType(TrashBreadcrumb));
expect(find.byType(TrashBreadcrumb), findsOneWidget);
// Navigate using the trash breadcrumb
await tester.tap(
find.descendant(
of: find.byType(TrashBreadcrumb),
matching: find.text(
LocaleKeys.trash_text.tr(),
),
),
);
await tester.pumpUntilFound(find.text(LocaleKeys.trash_restoreAll.tr()));
// Restore all
await tester.tap(find.text(LocaleKeys.trash_restoreAll.tr()));
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.trash_restore.tr()));
await tester.pumpAndSettle();
// Navigate back to the document
await tester.openPage('Getting started');
await tester.pumpAndSettle();
await tester.tap(mentionBlock);
await tester.pumpAndSettle();
expect(find.byType(TrashBreadcrumb), findsNothing);
});
});
}
@@ -1,378 +0,0 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
const _firstDocName = "Inline Sub Page Mention";
const _createdPageName = "hi world";
// Test cases that are covered in this file:
// - [x] Insert sub page mention from action menu (+)
// - [x] Delete sub page mention from editor
// - [x] Delete page from sidebar
// - [x] Delete page from sidebar and then trash
// - [x] Undo delete sub page mention
// - [x] Cut+paste in same document
// - [x] Cut+paste in different document
// - [x] Cut+paste in same document and then paste again in same document
// - [x] Turn paragraph with sub page mention into a heading
// - [x] Turn heading with sub page mention into a paragraph
// - [x] Duplicate a Block containing two sub page mentions
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document inline sub-page mention tests:', () {
testWidgets('Insert (& delete) a sub page mention from action menu',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Delete from editor
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
// Undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
// Move to trash (delete from sidebar)
await tester.rightClickOnPageName(_createdPageName);
await tester.tapButtonWithName(ViewMoreActionType.delete.name);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(
find.text(LocaleKeys.document_mention_trashHint.tr()),
findsOneWidget,
);
// Delete from trash
await tester.tapTrashButton();
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr()));
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.button_delete.tr()));
await tester.pumpAndSettle();
await tester.openPage(_firstDocName);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(
find.text(LocaleKeys.document_mention_deletedPage.tr()),
findsOneWidget,
);
});
testWidgets(
'Cut+paste in same document and cut+paste in different document',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Cut from editor
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
// Paste in same document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Cut again
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
// Create another document
const anotherDocName = "Another Document";
await tester.createOpenRenameDocumentUnderParent(
name: anotherDocName,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
// Paste in document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpUntilFound(find.byType(MentionSubPageBlock));
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
await tester.expandOrCollapsePage(
pageName: anotherDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
});
testWidgets(
'Cut+paste in same docuemnt and then paste again in same document',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Cut from editor
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyX,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNothing);
expect(find.byType(MentionSubPageBlock), findsNothing);
// Paste in same document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Paste again
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle(const Duration(seconds: 2));
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsNWidgets(2));
expect(find.text('$_createdPageName (copy)'), findsNWidgets(2));
});
testWidgets('Turn into w/ sub page mentions', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
await tester.expandOrCollapsePage(
pageName: _firstDocName,
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
final headingText = LocaleKeys.document_slashMenu_name_heading1.tr();
final paragraphText = LocaleKeys.document_slashMenu_name_text.tr();
// Turn into heading
await tester.editor.openTurnIntoMenu([0]);
await tester.tapButton(find.findTextInFlowyText(headingText));
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Turn into paragraph
await tester.editor.openTurnIntoMenu([0]);
await tester.tapButton(find.findTextInFlowyText(paragraphText));
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
});
testWidgets('Duplicate a block containing two sub page mentions',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
await tester.insertInlineSubPageFromPlusMenu();
// Copy paste it
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: 1),
),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyC,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
await tester.editor.updateSelection(
Selection.collapsed(Position(path: [0], offset: 1)),
);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
expect(find.text("$_createdPageName (copy)"), findsOneWidget);
expect(find.byType(MentionSubPageBlock), findsNWidgets(2));
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsNWidgets(2));
// Duplicate node from block action menu
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
await tester.pumpAndSettle();
expect(find.text(_createdPageName), findsOneWidget);
expect(find.text("$_createdPageName (copy)"), findsNWidgets(2));
expect(find.text("$_createdPageName (copy) (copy)"), findsOneWidget);
});
});
}
extension _InlineSubPageTestHelper on WidgetTester {
Future<void> insertInlineSubPageFromPlusMenu() async {
await editor.tapLineOfEditorAt(0);
await editor.showPlusMenu();
// Workaround to allow typing a document name
await FlowyTestKeyboard.simulateKeyDownEvent(
tester: this,
withKeyUp: true,
[
LogicalKeyboardKey.keyH,
LogicalKeyboardKey.keyI,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyW,
LogicalKeyboardKey.keyO,
LogicalKeyboardKey.keyR,
LogicalKeyboardKey.keyL,
LogicalKeyboardKey.keyD,
],
);
await FlowyTestKeyboard.simulateKeyDownEvent(
tester: this,
withKeyUp: true,
[LogicalKeyboardKey.enter],
);
await pumpUntilFound(find.byType(MentionSubPageBlock));
}
}
@@ -1,8 +1,3 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -12,23 +7,9 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// +, ... button beside the block component.
group('block option action:', () {
Future<void> turnIntoBlock(
WidgetTester tester,
Path path, {
required String menuText,
required String afterType,
}) async {
await tester.editor.openTurnIntoMenu(path);
await tester.tapButton(
find.findTextInFlowyText(menuText),
);
final node = tester.editor.getCurrentEditorState().getNodeAtPath(path);
expect(node?.type, afterType);
}
testWidgets('''click + to add a block after current selection,
and click + and option key to add a block before current selection''',
group('document with option action button', () {
testWidgets(
'click + to add a block after current selection, and click + and option key to add a block before current selection',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -59,122 +40,5 @@ void main() {
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
});
testWidgets('turn into - single line', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const name = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: name);
await tester.openPage(name);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('turn into');
// click the block option button to convert it to another blocks
final values = {
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_bulletedList.tr():
BulletedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_numberedList.tr():
NumberedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
LocaleKeys.document_slashMenu_name_todoList.tr():
TodoListBlockKeys.type,
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
};
for (final value in values.entries) {
final menuText = value.key;
final afterType = value.value;
await turnIntoBlock(
tester,
[0],
menuText: menuText,
afterType: afterType,
);
}
});
testWidgets('turn into - multi lines', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const name = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: name);
await tester.openPage(name);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('turn into 1');
await tester.ime.insertCharacter('\n');
await tester.ime.insertText('turn into 2');
// click the block option button to convert it to another blocks
final values = {
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_bulletedList.tr():
BulletedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_numberedList.tr():
NumberedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
LocaleKeys.document_slashMenu_name_todoList.tr():
TodoListBlockKeys.type,
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
};
for (final value in values.entries) {
final editorState = tester.editor.getCurrentEditorState();
editorState.selection = Selection(
start: Position(path: [0]),
end: Position(path: [1], offset: 2),
);
final menuText = value.key;
final afterType = value.value;
await turnIntoBlock(
tester,
[0],
menuText: menuText,
afterType: afterType,
);
}
});
testWidgets(
'selecting the parent should deselect all the child nodes as well',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const name = 'Test Document';
await tester.createNewPageWithNameUnderParent(name: name);
await tester.openPage(name);
// create a nested list
// Item 1
// Nested Item 1
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('Item 1');
await tester.ime.insertCharacter('\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.tab);
await tester.ime.insertText('Nested Item 1');
// select the 'Nested Item 1' and then tap the option button of the 'Item 1'
final editorState = tester.editor.getCurrentEditorState();
final selection = Selection.collapsed(
Position(path: [0, 0], offset: 1),
);
editorState.selection = selection;
await tester.pumpAndSettle();
expect(editorState.selection, selection);
await tester.editor.hoverAndClickOptionMenuButton([0]);
expect(editorState.selection, Selection.collapsed(Position(path: [0])));
},
);
});
}
@@ -26,16 +26,11 @@ import '../../shared/util.dart';
// - [x] Redo adding a SubPageBlock (Expect the view to be restored)
// - [x] Redo delete of a SubPageBlock (Expect the view to be moved to trash again)
// - [x] Renaming a child view (Expect the view name to be updated in the document)
// - [x] Deleting a view (to trash) linked to a SubPageBlock deleted the SubPageBlock (Expect the SubPageBlock to be deleted)
// - [x] Deleting a view (to trash) linked to a SubPageBlock shows a hint that the view is in trash (Expect a hint to be shown)
// - [x] Deleting a view (in trash) linked to a SubPageBlock deletes the SubPageBlock (Expect the SubPageBlock to be deleted)
// - [x] Duplicating a SubPageBlock node from Action Menu (Expect a new view is created under current view with same content and name + (copy))
// - [x] Dragging a SubPageBlock node to a new position in the document (Expect everything to be normal)
/// The defaut page name is empty, if we're looking for a "text" we can look for
/// [LocaleKeys.menuAppHeader_defaultNewPageName] but it won't work for eg. hoverOnPageName
/// as it looks at the text provided instead of the actual displayed text.
///
const _defaultPageName = "";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -71,9 +66,9 @@ void main() {
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -99,7 +94,8 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -155,7 +151,8 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -216,7 +213,8 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -260,7 +258,8 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -301,6 +300,39 @@ void main() {
expect(find.text('Child page (copy)'), findsNothing);
});
testWidgets('Undo adding a SubPageBlock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.tapLineOfEditorAt(0);
// Undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
expect(find.text('Child page'), findsNothing);
});
testWidgets('Undo delete of a SubPageBlock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -313,7 +345,8 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -337,6 +370,55 @@ void main() {
expect(find.byType(SubPageBlockComponent), findsOneWidget);
});
// Redo: undoing adding a subpage block, then redoing to bring it back
// -> Add a subpage block
// -> Undo
// -> Redo
testWidgets('Redo adding of a SubPageBlock', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.tapLineOfEditorAt(0);
// Undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
expect(find.text('Child page'), findsNothing);
// Redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isShiftPressed: true,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(find.text('Child page'), findsNWidgets(2));
});
// Redo: undoing deleting a subpage block, then redoing to delete it again
// -> Add a subpage block
// -> Delete
@@ -354,7 +436,8 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -393,7 +476,8 @@ void main() {
expect(find.text('Child page'), findsNothing);
});
testWidgets('Delete a view from sidebar', (tester) async {
testWidgets('Delete a view first to trash, then from trash',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
@@ -405,17 +489,38 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
final hintText = LocaleKeys.document_plugins_subPage_inTrashHint.tr();
expect(find.text(hintText), findsNothing);
await tester.hoverOnPageName('Child page');
await tester.tapDeletePageButton();
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text(hintText), findsOne);
// Go to trash
await tester.tapTrashButton();
await tester.pumpAndSettle();
// Tap on delete all button
await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr()));
await tester.pumpAndSettle();
// Tap ok to delete app pages in trash
await tester.tap(find.text(LocaleKeys.button_delete.tr()));
await tester.pumpAndSettle();
await tester.openPage('SubPageBlock');
await tester.pumpAndSettle();
expect(find.text('Child page'), findsNothing);
expect(find.byType(SubPageBlockComponent), findsNothing);
});
@@ -432,7 +537,8 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester
.hoverOnPageName(LocaleKeys.menuAppHeader_defaultNewPageName.tr());
await tester.renamePage('Child page');
await tester.pumpAndSettle();
@@ -472,6 +578,10 @@ void main() {
expect(afterNode.type, SubPageBlockKeys.type);
expect(afterNode.type, beforeNode.type);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
expect(
find.text(LocaleKeys.document_plugins_subPage_inTrashHint.tr()),
findsNothing,
);
});
});
}
@@ -492,10 +602,6 @@ extension _SubPageTestHelper on WidgetTester {
offset: 100,
);
// Navigate to the previous page to see the SubPageBlock
await openPage('SubPageBlock');
await pumpAndSettle();
await pumpUntilFound(find.byType(SubPageBlockComponent));
}
}
@@ -8,6 +8,7 @@ import 'document_create_and_delete_test.dart'
import 'document_inline_page_reference_test.dart'
as document_inline_page_reference_test;
import 'document_more_actions_test.dart' as document_more_actions_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_shortcuts_test.dart' as document_shortcuts_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
@@ -23,7 +24,7 @@ import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
void main() {
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
@@ -39,11 +40,11 @@ void main() {
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_test.main();
document_option_action_test.main();
document_with_image_block_test.main();
document_with_multi_image_block_test.main();
document_inline_page_reference_test.main();
document_more_actions_test.main();
document_with_file_test.main();
document_shortcuts_test.main();
// Don't add new tests here. Add them to document_test_runner_2.dart
}
@@ -1,23 +0,0 @@
import 'package:integration_test/integration_test.dart';
import 'document_app_lifecycle_test.dart' as document_app_lifecycle_test;
import 'document_deletion_test.dart' as document_deletion_test;
import 'document_option_action_test.dart' as document_option_action_test;
import 'document_title_test.dart' as document_title_test;
import 'document_with_date_reminder_test.dart'
as document_with_date_reminder_test;
import 'document_inline_sub_page_test.dart' as document_inline_sub_page_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_title_test.main();
// Disable subPage test temporarily, enable it in version 0.7.2
// document_sub_page_test.main();
document_app_lifecycle_test.main();
document_with_date_reminder_test.main();
document_deletion_test.main();
document_option_action_test.main();
document_inline_sub_page_test.main();
}
@@ -1,4 +1,3 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -222,85 +221,6 @@ void main() {
expect(newTitle, findsOneWidget);
});
testWidgets('execute undo and redo in title', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
// press a random key to make the undo stack not empty
await tester.simulateKeyEvent(LogicalKeyboardKey.keyA);
await tester.pumpAndSettle();
// undo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
// wait for the undo to be applied
await tester.pumpAndSettle(Durations.long1);
// expect the title is empty
expect(
tester
.widget<TextField>(
tester.editor.findDocumentTitle(''),
)
.controller
?.text,
'',
);
// redo
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
isShiftPressed: true,
);
await tester.pumpAndSettle(Durations.short1);
if (UniversalPlatform.isMacOS) {
expect(
tester
.widget<TextField>(
tester.editor.findDocumentTitle(_testDocumentName),
)
.controller
?.text,
_testDocumentName,
);
}
});
testWidgets('escape key should exit the editing mode', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.enterText(title, _testDocumentName);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(
tester
.widget<TextField>(
tester.editor.findDocumentTitle(_testDocumentName),
)
.focusNode
?.hasFocus,
isFalse,
);
});
testWidgets('press arrow down key in title, check if the cursor flashes',
(tester) async {
await tester.initializeAppFlowy();
@@ -328,24 +248,5 @@ void main() {
),
);
});
testWidgets(
'hover on the cover title, check if the add icon & add cover button are shown',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
final title = tester.editor.findDocumentTitle('');
await tester.hoverOnWidget(
title,
onHover: () async {
expect(find.byType(DocumentCoverWidget), findsOneWidget);
},
);
await tester.pumpAndSettle();
});
});
}
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -13,7 +14,7 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('cover image:', () {
group('cover image', () {
testWidgets('document cover tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -1,125 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
setUp(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
});
group('date or reminder block in document', () {
testWidgets("insert date with time block", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'Date with time test',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
offset: 100,
);
final dateTimeSettings = DateTimeSettingsPB(
dateFormat: UserDateFormatPB.Friendly,
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final DateTime currentDateTime = DateTime.now();
final String formattedDate =
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
// get current date in editor
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
// tap on date field
await tester.tap(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// tap the toggle of include time
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
// add time 11:12
final currentTime = DateFormat('HH:mm').format(DateTime.now());
final textField = find.byWidgetPredicate(
(widget) =>
widget is TextField && widget.controller!.text == currentTime,
);
await tester.enterText(textField, "11:12");
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
// we will get field with current date and 11:12 as time
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate 11:12'), findsOneWidget);
});
testWidgets("insert date with reminder block", (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'Date with reminder test',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
offset: 100,
);
final dateTimeSettings = DateTimeSettingsPB(
dateFormat: UserDateFormatPB.Friendly,
timeFormat: UserTimeFormatPB.TwentyFourHour,
);
final DateTime currentDateTime = DateTime.now();
final String formattedDate =
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
// get current date in editor
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
// tap on date field
await tester.tap(find.byType(MentionDateBlock));
await tester.pumpAndSettle();
// tap reminder and set reminder to 1 day before
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
await tester.pumpAndSettle();
await tester.tap(
find.textContaining(
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
),
);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
// we will get field with current date reminder_clock.svg icon
expect(find.byType(MentionDateBlock), findsOneWidget);
expect(find.text('@$formattedDate'), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
});
});
}
@@ -6,6 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/startup/startup.dart';
@@ -159,7 +160,10 @@ void main() {
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
expect(find.text('Unsplash'), findsOneWidget);
await tester.tapButtonWithName('Unsplash');
await tester.pumpUntilFound(find.byType(UnsplashImageWidget));
expect(find.byType(UnsplashImageWidget), findsOneWidget);
});
});
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -92,7 +92,7 @@ void main() {
);
expect(finder, findsOneWidget);
await tester.tapButton(finder);
expect(find.byType(GridPage), findsOneWidget);
expect(find.byType(AppFlowyErrorPage), findsOneWidget);
});
});
}
@@ -1,6 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -179,9 +182,13 @@ Future<void> hoverAndClickDepthOptionAction(
List<int> path,
int level,
) async {
await tester.editor.openDepthMenu(path);
final type = OptionDepthType.fromLevel(level);
await tester.tapButton(find.findTextInFlowyText(type.description));
await tester.editor.hoverAndClickOptionMenuButton([3]);
await tester.tap(find.byType(AppFlowyPopover).hitTestable().last);
await tester.pumpAndSettle();
// Find a total of 4 HoverButtons under the [BlockOptionButton],
// in addition to 3 HoverButtons under the [DepthOptionAction] - (child of BlockOptionButton)
await tester.tap(find.byType(HoverButton).hitTestable().at(3 + level));
await tester.pumpAndSettle();
}
@@ -189,46 +189,15 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and click the delete button
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.pumpAndSettle();
await tester.tapDeleteOnRowMenu();
await tester.pumpAndSettle();
// 3 initial rows - 1 deleted
tester.assertNumberOfRowsInGridPage(2);
});
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
tester.assertNumberOfRowsInGridPage(2);
});
testWidgets('delete row in two views', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.renameLinkedView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid),
'grid 1',
);
tester.assertNumberOfRowsInGridPage(3);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
await tester.renameLinkedView(
tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid).at(1),
'grid 2',
);
tester.assertNumberOfRowsInGridPage(3);
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and click the delete button
await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu();
});
expect(find.byType(ConfirmPopup), findsOneWidget);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
// 3 initial rows - 1 deleted
tester.assertNumberOfRowsInGridPage(2);
await tester.tapTabBarLinkedViewByViewName('grid 1');
tester.assertNumberOfRowsInGridPage(2);
});
});
}
@@ -11,7 +11,7 @@ void main() {
const emoji = '😁';
group('Icon:', () {
group('Icon', () {
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -52,7 +52,6 @@ void main() {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
@@ -198,7 +198,7 @@ void main() {
layout: ViewLayoutPB.Grid,
onHover: () async {
expect(find.byType(ViewAddButton), findsNothing);
expect(find.byType(ViewMoreActionPopover), findsOneWidget);
expect(find.byType(ViewMoreActionButton), findsOneWidget);
},
);
});
@@ -3,7 +3,6 @@ import 'package:integration_test/integration_test.dart';
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_test.dart' as sidebar_test;
import 'sidebar_view_item_test.dart' as sidebar_view_item_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -13,5 +12,4 @@ void main() {
// sidebar_expanded_test.main();
sidebar_favorite_test.main();
sidebar_icon_test.main();
sidebar_view_item_test.main();
}
@@ -1,39 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Sidebar view item tests', () {
testWidgets('Access view item context menu by right click', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// Right click on the view item and change icon
await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);
await tester.pumpAndSettle();
// Change icon
final changeIconButton =
find.text(LocaleKeys.document_plugins_cover_changeIcon.tr());
await tester.tapButton(changeIconButton);
await tester.pumpUntilFound(find.byType(FlowyEmojiPicker));
const emoji = '😁';
await tester.tapEmoji(emoji);
await tester.pumpAndSettle();
tester.expectViewHasIcon(gettingStarted, ViewLayoutPB.Document, emoji);
});
});
}
@@ -1,6 +1,6 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_1.dart' as document_test_runner;
import 'desktop/document/document_test_runner.dart' as document_test_runner;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test;
@@ -15,7 +15,7 @@ Future<void> runIntegration1OnDesktop() async {
first_test.main();
switch_folder_test.main();
document_test_runner.main();
document_test_runner.startTesting();
// DON'T add more tests here.
// DON'T add more tests here. This is the first test runner for desktop.
}
@@ -3,12 +3,16 @@ import 'package:integration_test/integration_test.dart';
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/database/database_row_cover_test.dart'
as database_row_cover_test;
import 'desktop/grid/grid_edit_row_test.dart' as grid_edit_row_test_runner;
import 'desktop/document/document_title_test.dart' as document_title_test;
import 'desktop/document/document_app_lifecycle_test.dart'
as document_app_lifecycle_test;
import 'desktop/document/document_sub_page_test.dart' as document_sub_page_test;
import 'desktop/grid/grid_filter_and_sort_test.dart'
as grid_filter_and_sort_test_runner;
import 'desktop/grid/grid_reopen_test.dart' as grid_reopen_test_runner;
import 'desktop/grid/grid_reorder_row_test.dart'
as grid_reorder_row_test_runner;
import 'desktop/grid/grid_edit_row_test.dart' as grid_edit_row_test_runner;
import 'desktop/grid/grid_row_test.dart' as grid_create_row_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
@@ -47,6 +51,7 @@ Future<void> runIntegration3OnDesktop() async {
grid_filter_and_sort_test_runner.main();
grid_edit_row_test_runner.main();
zoom_in_out_test.main();
// DON'T add more tests here. Add them to desktop_runner_4.dart
document_title_test.main();
document_sub_page_test.main();
document_app_lifecycle_test.main();
}
@@ -1,17 +0,0 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_2.dart' as document_test_runner_2;
import 'desktop/uncategorized/empty_test.dart' as first_test;
Future<void> main() async {
await runIntegration4OnDesktop();
}
Future<void> runIntegration4OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
document_test_runner_2.main();
}
@@ -1,9 +1,5 @@
import 'document/publish_test.dart' as publish_test;
import 'document/share_link_test.dart' as share_link_test;
import 'workspace/workspace_operations_test.dart' as workspace_operations_test;
Future<void> main() async {
workspace_operations_test.main();
share_link_test.main();
publish_test.main();
}
@@ -1,89 +0,0 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'dart:math';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/shared/patterns/common_patterns.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/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('publish:', () {
testWidgets('publish document', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openPage(Constants.gettingStartedPageName);
await tester.editor.openMoreActionMenuOnMobile();
// click the publish button
await tester.editor.clickMoreActionItemOnMobile(
LocaleKeys.shareAction_publish.tr(),
);
// wait the notification dismiss
final publishSuccessText = find.findTextInFlowyText(
LocaleKeys.publish_publishSuccessfully.tr(),
);
expect(publishSuccessText, findsOneWidget);
await tester.pumpUntilNotFound(publishSuccessText);
// open the menu again, to check the publish status
await tester.editor.openMoreActionMenuOnMobile();
// expect to see the unpublish button and the visit site button
expect(
find.text(LocaleKeys.shareAction_unPublish.tr()),
findsOneWidget,
);
expect(
find.text(LocaleKeys.shareAction_visitSite.tr()),
findsOneWidget,
);
// unpublish the document
await tester.editor.clickMoreActionItemOnMobile(
LocaleKeys.shareAction_unPublish.tr(),
);
final unPublishSuccessText = find.findTextInFlowyText(
LocaleKeys.publish_unpublishSuccessfully.tr(),
);
expect(unPublishSuccessText, findsOneWidget);
await tester.pumpUntilNotFound(unPublishSuccessText);
});
});
}
@@ -1,69 +0,0 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/shared/patterns/common_patterns.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/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('share link:', () {
testWidgets('copy share link and paste it on doc', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open the getting started page and paste the link
await tester.openPage(Constants.gettingStartedPageName);
// open the more action menu
await tester.editor.openMoreActionMenuOnMobile();
// click the share link item
await tester.editor.clickMoreActionItemOnMobile(
LocaleKeys.shareAction_copyLink.tr(),
);
// check the clipboard
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text,
matches(appflowySharePageLinkPattern),
);
});
});
}
@@ -1,12 +0,0 @@
import 'package:integration_test/integration_test.dart';
import 'page_style_test.dart' as page_style_test;
import 'title_test.dart' as title_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
title_test.main();
page_style_test.main();
}
@@ -57,11 +57,10 @@ void main() {
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.height ??
PageStyleLineHeightLayout.normal.lineHeight;
.style()
.textStyleConfiguration
.text
.height!;
}
testWidgets('change font size in page style settings', (tester) async {
@@ -127,6 +126,7 @@ void main() {
// click done button to exit the page style settings
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
// check the cover
final builtInCover = find.descendant(
@@ -1,76 +0,0 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.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/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package: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/dir.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document title:', () {
testWidgets('create a new page, the title should be empty', (tester) async {
await tester.launchInAnonymousMode();
final createPageButton = find.byKey(
BottomNavigationBarItemType.add.valueKey,
);
await tester.tapButton(createPageButton);
expect(find.byType(MobileDocumentScreen), findsOneWidget);
final title = tester.editor.findDocumentTitle('');
expect(title, findsOneWidget);
final textField = tester.widget<TextField>(title);
expect(textField.focusNode!.hasFocus, isTrue);
// input new name and press done button
const name = 'test document';
await tester.enterText(title, name);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
final newTitle = tester.editor.findDocumentTitle(name);
expect(newTitle, findsOneWidget);
expect(textField.controller!.text, name);
// the document should get focus
final editor = tester.widget<AppFlowyEditorPage>(
find.byType(AppFlowyEditorPage),
);
expect(
editor.editorState.selection,
Selection.collapsed(Position(path: [0])),
);
});
});
}
@@ -3,11 +3,10 @@
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
@@ -35,12 +34,13 @@ void main() {
cloudType: AuthenticatorType.local,
);
// click the anonymousSignInButton
final anonymousSignInButton = find.byType(SignInAnonymousButtonV2);
expect(anonymousSignInButton, findsOneWidget);
await tester.tapButton(anonymousSignInButton);
// tap the create page button
final createPageButton = find.byWidgetPredicate(
(widget) =>
widget is FlowySvg &&
widget.svg.path == FlowySvgs.m_home_unselected_m.path,
);
final createPageButton = find.byKey(mobileCreateNewPageButtonKey);
await tester.tapButton(createPageButton);
expect(find.byType(MobileDocumentScreen), findsOneWidget);
});
@@ -29,6 +29,11 @@ void main() {
testWidgets('anon user and then sign in', (tester) async {
await tester.initializeAppFlowy();
// click the anonymousSignInButton
final anonymousSignInButton = find.byType(SignInAnonymousButtonV2);
expect(anonymousSignInButton, findsOneWidget);
await tester.tapButton(anonymousSignInButton);
// expect to see the home page
expect(find.byType(MobileHomeScreen), findsOneWidget);
});
@@ -1,4 +1,3 @@
import 'package:appflowy_backend/log.dart';
import 'package:integration_test/integration_test.dart';
import 'mobile/document/page_style_test.dart' as page_style_test;
@@ -6,9 +5,7 @@ import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
Future<void> runIntegrationOnMobile() async {
Log.shared.disableLog = true;
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main();
create_new_page_test.main();
page_style_test.main();
@@ -3,7 +3,6 @@ import 'dart:io';
import 'desktop_runner_1.dart';
import 'desktop_runner_2.dart';
import 'desktop_runner_3.dart';
import 'desktop_runner_4.dart';
import 'mobile_runner.dart';
/// The main task runner for all integration tests in AppFlowy.
@@ -18,7 +17,6 @@ Future<void> main() async {
await runIntegration1OnDesktop();
await runIntegration2OnDesktop();
await runIntegration3OnDesktop();
await runIntegration4OnDesktop();
} else if (Platform.isIOS || Platform.isAndroid) {
await runIntegrationOnMobile();
} else {
@@ -17,7 +17,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:universal_platform/universal_platform.dart';
class FlowyTestContext {
FlowyTestContext({required this.applicationDataDirectory});
@@ -106,7 +105,7 @@ extension AppFlowyTestBase on WidgetTester {
}
Future<void> waitUntilSignInPageShow() async {
if (isAuthEnabled || UniversalPlatform.isMobile) {
if (isAuthEnabled) {
final finder = find.byType(SignInAnonymousButtonV2);
await pumpUntilFound(finder, timeout: const Duration(seconds: 30));
expect(finder, findsOneWidget);
@@ -159,12 +158,17 @@ extension AppFlowyTestBase on WidgetTester {
Future<void> tapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = false,
int milliseconds = 500,
bool pumpAndSettle = true,
}) async {
await tap(finder, buttons: buttons, warnIfMissed: warnIfMissed);
await tap(
finder,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
if (pumpAndSettle) {
await this.pumpAndSettle(
@@ -1,5 +1,11 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -8,16 +14,13 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
@@ -37,13 +40,8 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'emoji.dart';
import 'util.dart';
@@ -187,16 +185,6 @@ extension CommonOperations on WidgetTester {
}
}
/// Right click on the page name.
Future<void> rightClickOnPageName(
String name, {
ViewLayoutPB layout = ViewLayoutPB.Document,
}) async {
final page = findPageName(name, layout: layout);
await tap(page, buttons: kSecondaryMouseButton);
await pumpAndSettle();
}
/// open the page with given name.
Future<void> openPage(
String name, {
@@ -211,11 +199,8 @@ extension CommonOperations on WidgetTester {
///
/// Must call [hoverOnPageName] first.
Future<void> tapPageOptionButton() async {
final optionButton = find.descendant(
of: find.byType(ViewMoreActionPopover),
matching: find.byFlowySvg(FlowySvgs.workspace_three_dots_s),
);
await tapButton(optionButton, warnIfMissed: true);
final optionButton = find.byType(ViewMoreActionButton);
await tapButton(optionButton);
}
/// Tap the delete page button.
@@ -294,14 +279,12 @@ extension CommonOperations on WidgetTester {
/// Tap the delete permanently button.
///
/// the delete permanently button will show after the current page is deleted.
/// the restore button will show after the current page is deleted.
Future<void> tapDeletePermanentlyButton() async {
final deleteButton = find.textContaining(
final restoreButton = find.textContaining(
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
);
await tapButton(deleteButton);
await tap(find.text(LocaleKeys.button_delete.tr()));
await pumpAndSettle();
await tapButton(restoreButton);
}
/// Tap the share button above the document page.
@@ -341,10 +324,14 @@ extension CommonOperations on WidgetTester {
}
await pumpAndSettle();
final defaultPageName = layout == ViewLayoutPB.Document
? '' // the document name is empty by default
: LocaleKeys.menuAppHeader_defaultNewPageName.tr();
// hover on it and change it's name
if (name != null) {
await hoverOnPageName(
layout.defaultName,
defaultPageName,
layout: layout,
onHover: () async {
await renamePage(name);
@@ -358,39 +345,13 @@ extension CommonOperations on WidgetTester {
if (openAfterCreated) {
await openPage(
// if the name is null, use the default name
name ?? layout.defaultName,
name ?? defaultPageName,
layout: layout,
);
await pumpAndSettle();
}
}
Future<void> createOpenRenameDocumentUnderParent({
required String name,
String? parentName,
}) async {
// create a new page
await tapAddViewButton(name: parentName ?? gettingStarted);
await tapButtonWithName(ViewLayoutPB.Document.menuName);
final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
KVKeys.showRenameDialogWhenCreatingNewFile,
(value) => bool.parse(value),
);
final showRenameDialog = settingsOrFailure ?? false;
if (showRenameDialog) {
await tapOKButton();
}
await pumpAndSettle();
// open the page after created
await openPage(ViewLayoutPB.Document.defaultName);
await pumpAndSettle();
// Enter new name in the document title
await enterText(find.byType(TextFieldWithMetricLines), name);
await pumpAndSettle();
}
/// Create a new page in the space
Future<void> createNewPageInSpace({
required String spaceName,
@@ -1,8 +1,6 @@
import 'dart:io';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:flutter/gestures.dart';
@@ -413,11 +411,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> selectOption({required String name}) async {
final option = find.descendant(
of: find.byType(SelectOptionCellEditor),
matching: find.byWidgetPredicate(
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
),
final option = find.byWidgetPredicate(
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
);
await tapButton(option);
@@ -509,7 +504,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> renameChecklistTask({
required int index,
required String name,
bool enter = true,
}) async {
final textField = find
.descendant(
@@ -519,9 +513,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
.at(index);
await enterText(textField, name);
if (enter) {
await testTextInput.receiveAction(TextInputAction.done);
}
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
}
@@ -539,32 +531,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> deleteChecklistTask({required int index}) async {
final task = find.byType(ChecklistItem).at(index);
await hoverOnWidget(
task,
onHover: () async {
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
);
await tapButton(button);
},
);
}
await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
await pumpAndSettle();
void assertPhantomChecklistItemAtIndex({required int index}) {
final paddings = find.descendant(
of: find.descendant(
of: find.byType(ChecklistRowDetailCell),
matching: find.byType(ReorderableListView),
),
matching: find.byWidgetPredicate(
(widget) =>
widget is Padding &&
(widget.child is ChecklistItem ||
widget.child is PhantomChecklistItem),
),
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
);
final phantom = widget<Padding>(paddings.at(index)).child!;
expect(phantom is PhantomChecklistItem, true);
await tapButton(button);
}
Future<void> openFirstRowDetailPage() async {
@@ -1183,6 +1157,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(ChecklistFilterConditionList));
}
Future<void> tapDateFilterButtonInGrid() async {
await tapButton(find.byType(DateFilterConditionList));
}
/// The [SelectOptionFilterList] must show up first.
Future<void> tapOptionFilterWithName(String name) async {
final findCell = find.descendant(
@@ -1216,36 +1194,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> changeTextFilterCondition(
TextFilterConditionPB condition,
) async {
await tapButton(find.byType(TextFilterConditionList));
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(
condition.filterName,
),
);
await tapButton(button);
}
Future<void> changeSelectFilterCondition(
SelectOptionFilterConditionPB condition,
) async {
await tapButton(find.byType(SelectOptionFilterConditionList));
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(condition.i18n),
);
await tapButton(button);
}
Future<void> changeDateFilterCondition(
DateTimeFilterCondition condition,
) async {
await tapButton(find.byType(DateFilterConditionList));
Future<void> tapDateFilterCondition(DateTimeFilterCondition condition) async {
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(condition.filterName),
@@ -1,15 +1,14 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_title.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
@@ -18,14 +17,10 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/uplo
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_platform/universal_platform.dart';
import 'util.dart';
@@ -75,20 +70,6 @@ class EditorOperations {
expect(find.byType(FlowyEmojiPicker), findsOneWidget);
}
Future<void> paste() async {
if (UniversalPlatform.isMacOS) {
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isMetaPressed: true,
);
} else {
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: true,
);
}
}
Future<void> tapGettingStartedIcon() async {
await tester.tapButton(
find.descendant(
@@ -197,11 +178,6 @@ class EditorOperations {
await tester.ime.insertCharacter('@');
}
/// trigger the plus action menu (+) command
Future<void> showPlusMenu() async {
await tester.ime.insertCharacter('+');
}
/// Tap the slash menu item with [name]
///
/// Must call [showSlashMenu] first.
@@ -298,42 +274,10 @@ class EditorOperations {
widget.blockComponentContext.node.path.equals(path),
),
);
await tester.pumpUntilFound(find.byType(PopoverActionList));
},
);
}
/// open the turn into menu
Future<void> openTurnIntoMenu(Path path) async {
await hoverAndClickOptionMenuButton(path);
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
),
);
await tester.pumpUntilFound(find.byType(TurnIntoOptionMenu));
}
/// copy link to block
Future<void> copyLinkToBlock(Path path) async {
await hoverAndClickOptionMenuButton(path);
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),
),
);
}
Future<void> openDepthMenu(Path path) async {
await hoverAndClickOptionMenuButton(path);
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_depth.tr(),
),
);
await tester.pumpUntilFound(find.byType(DepthOptionMenu));
}
/// Drag block
///
/// [offset] is the offset to move the block.
@@ -385,13 +329,9 @@ class EditorOperations {
await tester.pumpAndSettle(Durations.short1);
}
Finder findDocumentTitle(String? title) {
final parent = UniversalPlatform.isDesktop
? find.byType(CoverTitle)
: find.byType(DocumentImmersiveCover);
Finder findDocumentTitle(String title) {
return find.descendant(
of: parent,
of: find.byType(CoverTitle),
matching: find.byWidgetPredicate(
(widget) {
if (widget is! TextField) {
@@ -402,10 +342,6 @@ class EditorOperations {
return true;
}
if (title == null) {
return true;
}
if (title.isEmpty) {
return widget.controller?.text.isEmpty ?? false;
}
@@ -415,23 +351,4 @@ class EditorOperations {
),
);
}
/// open the more action menu on mobile
Future<void> openMoreActionMenuOnMobile() async {
final moreActionButton = find.byType(MobileViewPageMoreButton);
await tester.tapButton(moreActionButton);
await tester.pumpAndSettle();
}
/// click the more action item on mobile
///
/// rename, add collaborator, publish, delete, etc.
Future<void> clickMoreActionItemOnMobile(String name) async {
final moreActionItem = find.descendant(
of: find.byType(MobileQuickActionButton),
matching: find.findTextInFlowyText(name),
);
await tester.tapButton(moreActionItem);
await tester.pumpAndSettle();
}
}
@@ -1,7 +1,7 @@
import 'package:appflowy/startup/tasks/device_info_task.dart';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:universal_platform/universal_platform.dart';
class CocoaWindowChannel {
CocoaWindowChannel._();
@@ -44,14 +44,7 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
@override
Widget build(BuildContext context) {
// the frameless window is only supported on macOS
if (!UniversalPlatform.isMacOS) {
return widget.child ?? const SizedBox.shrink();
}
// For the macOS version 15 or higher, we can control the window position by using system APIs
if (ApplicationInfo.macOSMajorVersion != null &&
ApplicationInfo.macOSMajorVersion! >= 15) {
if (!Platform.isMacOS) {
return widget.child ?? const SizedBox.shrink();
}
+1 -3
View File
@@ -3,9 +3,7 @@ import 'package:scaled_app/scaled_app.dart';
import 'startup/startup.dart';
Future<void> main() async {
ScaledWidgetsFlutterBinding.ensureInitialized(
scaleFactor: (_) => 1.0,
);
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0);
await runAppFlowy();
}
@@ -56,7 +56,7 @@ class NotificationReminderBloc
emit(
NotificationReminderState(
createdAt: createdAt,
pageTitle: view.nameOrDefault,
pageTitle: view.name,
view: view,
reminderContent: node.delta?.toPlainText() ?? '',
nodes: [node],
@@ -68,7 +68,7 @@ class NotificationReminderBloc
emit(
NotificationReminderState(
createdAt: createdAt,
pageTitle: view.nameOrDefault,
pageTitle: view.name,
view: view,
reminderContent: reminder.message,
status: NotificationReminderStatus.loaded,
@@ -441,15 +441,4 @@ class PageStyleCover {
bool get isCustomImage => type == PageStyleCoverImageType.customImage;
bool get isUnsplashImage => type == PageStyleCoverImageType.unsplashImage;
bool get isLocalImage => type == PageStyleCoverImageType.localImage;
@override
bool operator ==(Object other) {
if (other is! PageStyleCover) {
return false;
}
return type == other.type && value == other.value;
}
@override
int get hashCode => Object.hash(type, value);
}
@@ -5,7 +5,6 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@@ -95,10 +94,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
BlocProvider.value(
value: getIt<ReminderBloc>(),
),
BlocProvider(
create: (_) =>
ShareBloc(view: view)..add(const ShareEvent.initial()),
),
if (view.layout.isDocumentView)
BlocProvider(
create: (_) => DocumentPageStyleBloc(view: view)
@@ -8,7 +8,6 @@ import 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.da
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -99,8 +98,6 @@ class MobileViewPageMoreButton extends StatelessWidget {
providers: [
BlocProvider.value(value: context.read<ViewBloc>()),
BlocProvider.value(value: context.read<FavoriteBloc>()),
BlocProvider.value(value: context.read<MobileViewPageBloc>()),
BlocProvider.value(value: context.read<ShareBloc>()),
],
child: MobileViewPageMoreBottomSheet(view: view),
),
@@ -1,24 +1,11 @@
import 'dart:async';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/plugins/shared/share/publish_name_generator.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/navigator_context_exntesion.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
class MobileViewPageMoreBottomSheet extends StatelessWidget {
const MobileViewPageMoreBottomSheet({super.key, required this.view});
@@ -29,20 +16,29 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
Widget build(BuildContext context) {
return ViewPageBottomSheet(
view: view,
onAction: (action) async {
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
// show toast
break;
case MobileViewBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
// pop to home page
context
..pop()
..pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
context.popToHome();
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
context.pop();
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
@@ -56,125 +52,17 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.publish:
await _publish(context);
if (context.mounted) {
context.pop();
}
break;
case MobileViewBottomSheetBodyAction.unpublish:
_unpublish(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyPublishLink:
_copyPublishLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.visitSite:
_visitPublishedSite(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyShareLink:
_copyShareLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
_onRename(context, name);
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
Future<void> _publish(BuildContext context) async {
final id = context.read<ShareBloc>().view.id;
final publishName = await generatePublishName(
id,
view.name,
);
if (context.mounted) {
context.read<ShareBloc>().add(
ShareEvent.publish(
'',
publishName,
[view.id],
),
);
showToastNotification(
context,
message: LocaleKeys.publish_publishSuccessfully.tr(),
);
}
}
void _unpublish(BuildContext context) {
context.read<ShareBloc>().add(const ShareEvent.unPublish());
showToastNotification(
context,
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
);
}
void _copyPublishLink(BuildContext context) {
final url = context.read<ShareBloc>().state.url;
if (url.isNotEmpty) {
unawaited(
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: url),
),
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copy.tr(),
);
}
}
void _visitPublishedSite(BuildContext context) {
final url = context.read<ShareBloc>().state.url;
if (url.isNotEmpty) {
unawaited(
afLaunchUrl(
Uri.parse(url),
mode: LaunchMode.externalApplication,
),
);
}
}
void _copyShareLink(BuildContext context) {
final workspaceId = context.read<ShareBloc>().state.workspaceId;
final viewId = context.read<ShareBloc>().state.viewId;
final url = ShareConstants.buildShareUrl(
workspaceId: workspaceId,
viewId: viewId,
);
if (url.isNotEmpty) {
unawaited(
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: url),
),
);
showToastNotification(
context,
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
);
} else {
showToastNotification(
context,
message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(),
type: ToastificationType.error,
);
}
}
void _onRename(BuildContext context, String name) {
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
}
}
@@ -1,29 +1,21 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
enum MobileViewBottomSheetBodyAction {
undo,
redo,
share,
rename,
duplicate,
delete,
addToFavorites,
removeFromFavorites,
helpCenter,
publish,
unpublish,
copyPublishLink,
visitSite,
copyShareLink,
helpCenter;
}
typedef MobileViewBottomSheetBodyActionCallback = void Function(
@@ -98,7 +90,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileQuickActionButton(
text: LocaleKeys.button_rename.tr(),
icon: FlowySvgs.view_item_rename_s,
iconSize: const Size.square(18),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.rename,
),
@@ -109,7 +100,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
? LocaleKeys.button_removeFromFavorites.tr()
: LocaleKeys.button_addToFavorites.tr(),
icon: isFavorite ? FlowySvgs.unfavorite_s : FlowySvgs.favorite_s,
iconSize: const Size.square(18),
onTap: () => onAction(
isFavorite
? MobileViewBottomSheetBodyAction.removeFromFavorites
@@ -120,30 +110,16 @@ class MobileViewBottomSheetBody extends StatelessWidget {
MobileQuickActionButton(
text: LocaleKeys.button_duplicate.tr(),
icon: FlowySvgs.duplicate_s,
iconSize: const Size.square(18),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.duplicate,
),
),
// copy link
_divider(),
MobileQuickActionButton(
text: LocaleKeys.shareAction_copyLink.tr(),
icon: FlowySvgs.m_copy_link_s,
iconSize: const Size.square(18),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.copyShareLink,
),
),
_divider(),
..._buildPublishActions(context),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
icon: FlowySvgs.trash_s,
iconColor: Theme.of(context).colorScheme.error,
iconSize: const Size.square(18),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.delete,
),
@@ -153,48 +129,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
);
}
List<Widget> _buildPublishActions(BuildContext context) {
final userProfile = context.read<MobileViewPageBloc>().state.userProfilePB;
// the publish feature is only available for AppFlowy Cloud
if (userProfile == null ||
userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) {
return [];
}
final isPublished = context.watch<ShareBloc>().state.isPublished;
if (isPublished) {
return [
MobileQuickActionButton(
text: LocaleKeys.shareAction_visitSite.tr(),
icon: FlowySvgs.m_visit_site_s,
iconSize: const Size.square(18),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.visitSite,
),
),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.shareAction_unPublish.tr(),
icon: FlowySvgs.m_unpublish_s,
iconSize: const Size.square(18),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.unpublish,
),
),
];
} else {
return [
MobileQuickActionButton(
text: LocaleKeys.shareAction_publish.tr(),
icon: FlowySvgs.m_publish_s,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.publish,
),
),
];
}
}
Widget _divider() => const Divider(
height: 8.5,
thickness: 0.5,
@@ -7,7 +7,6 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -94,7 +93,7 @@ enum MobilePaneActionType {
Navigator.of(sheetContext).pop();
viewBloc.add(
ViewEvent.createView(
layout.defaultName,
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout,
section: spaceType!.toViewSectionPB,
),
@@ -1003,7 +1003,7 @@ class _SelectOptionFilterContentEditorState
isSelected,
);
},
indicator: MobileSelectedOptionIndicator.multi,
indicator: _getIndicator(),
showMoreOptionsButton: false,
);
},
@@ -1027,19 +1027,35 @@ class _SelectOptionFilterContentEditorState
}
}
MobileSelectedOptionIndicator _getIndicator() {
return (widget.filter.condition == SelectOptionFilterConditionPB.OptionIs ||
widget.filter.condition ==
SelectOptionFilterConditionPB.OptionIsNot) &&
widget.field.fieldType == FieldType.SingleSelect
? MobileSelectedOptionIndicator.single
: MobileSelectedOptionIndicator.multi;
}
void _onTapHandler(
BuildContext context,
List<SelectOptionPB> options,
SelectOptionPB option,
bool isSelected,
) {
final selectedOptionIds = Set<String>.from(widget.filter.optionIds);
if (isSelected) {
selectedOptionIds.remove(option.id);
final selectedOptionIds = Set<String>.from(widget.filter.optionIds)
..remove(option.id);
_updateSelectOptions(context, options, selectedOptionIds);
} else {
selectedOptionIds.add(option.id);
final selectedOptionIds = widget.delegate.selectOption(
widget.filter.optionIds,
option.id,
widget.filter.condition,
);
_updateSelectOptions(context, options, selectedOptionIds);
}
_updateSelectOptions(context, options, selectedOptionIds);
}
void _updateSelectOptions(
@@ -93,6 +93,8 @@ final class SingleSelectOptionFilterCondition
return [
SelectOptionFilterConditionPB.OptionIs,
SelectOptionFilterConditionPB.OptionIsNot,
SelectOptionFilterConditionPB.OptionContains,
SelectOptionFilterConditionPB.OptionDoesNotContain,
SelectOptionFilterConditionPB.OptionIsEmpty,
SelectOptionFilterConditionPB.OptionIsNotEmpty,
].map((e) => (e, e.i18n)).toList();
@@ -107,6 +109,8 @@ final class MultiSelectOptionFilterCondition
return [
SelectOptionFilterConditionPB.OptionContains,
SelectOptionFilterConditionPB.OptionDoesNotContain,
SelectOptionFilterConditionPB.OptionIs,
SelectOptionFilterConditionPB.OptionIsNot,
SelectOptionFilterConditionPB.OptionIsEmpty,
SelectOptionFilterConditionPB.OptionIsNotEmpty,
].map((e) => (e, e.i18n)).toList();
@@ -26,6 +26,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:sentry/sentry.dart';
import 'package:toastification/toastification.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key});
@@ -93,7 +93,7 @@ class MobileSpace extends StatelessWidget {
Navigator.of(sheetContext).pop();
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: layout.defaultName,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
index: 0,
),
@@ -1,3 +1,4 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart';
import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart';
@@ -10,10 +11,10 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -186,7 +187,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: layout.defaultName,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
),
);
@@ -194,7 +195,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
// only support create document in section
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: layout.defaultName,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: FolderSpaceType.public.toViewSectionPB,
),
@@ -29,41 +29,23 @@ final PropertyValueNotifier<ViewLayoutPB?> mobileCreateNewPageNotifier =
final ValueNotifier<BottomNavigationBarActionType> bottomNavigationBarType =
ValueNotifier(BottomNavigationBarActionType.home);
enum BottomNavigationBarItemType {
home,
add,
notification;
String get label {
return switch (this) {
BottomNavigationBarItemType.home => 'home',
BottomNavigationBarItemType.add => 'add',
BottomNavigationBarItemType.notification => 'notification',
};
}
ValueKey get valueKey {
return ValueKey(label);
}
}
const _homeLabel = 'home';
const _addLabel = 'add';
const _notificationLabel = 'notification';
final _items = <BottomNavigationBarItem>[
BottomNavigationBarItem(
key: BottomNavigationBarItemType.home.valueKey,
label: BottomNavigationBarItemType.home.label,
icon: const FlowySvg(FlowySvgs.m_home_unselected_m),
activeIcon: const FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),
const BottomNavigationBarItem(
label: _homeLabel,
icon: FlowySvg(FlowySvgs.m_home_unselected_m),
activeIcon: FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),
),
BottomNavigationBarItem(
key: BottomNavigationBarItemType.add.valueKey,
label: BottomNavigationBarItemType.add.label,
icon: const FlowySvg(FlowySvgs.m_home_add_m),
const BottomNavigationBarItem(
label: _addLabel,
icon: FlowySvg(FlowySvgs.m_home_add_m),
),
BottomNavigationBarItem(
key: BottomNavigationBarItemType.notification.valueKey,
label: BottomNavigationBarItemType.notification.label,
icon: const _NotificationNavigationBarItemIcon(),
activeIcon: const _NotificationNavigationBarItemIcon(
const BottomNavigationBarItem(
label: _notificationLabel,
icon: _NotificationNavigationBarItemIcon(),
activeIcon: _NotificationNavigationBarItemIcon(
isActive: true,
),
),
@@ -252,11 +234,11 @@ class _HomePageNavigationBar extends StatelessWidget {
closePopupMenu();
final label = _items[bottomBarIndex].label;
if (label == BottomNavigationBarItemType.add.label) {
if (label == _addLabel) {
// show an add dialog
mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
return;
} else if (label == BottomNavigationBarItemType.notification.label) {
} else if (label == _notificationLabel) {
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
}
// When navigating to a new branch, it's recommended to use the goBranch
@@ -258,7 +258,7 @@ class NotificationDocumentContent extends StatelessWidget {
),
);
final blockBuilders = buildBlockComponentBuilders(
final blockBuilders = getEditorBuilderMap(
context: context,
editorState: editorState,
styleCustomizer: styleCustomizer,
@@ -260,7 +260,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
// title
Expanded(
child: FlowyText.regular(
widget.view.nameOrDefault,
widget.view.name,
fontSize: 16.0,
figmaLineHeight: 20.0,
overflow: TextOverflow.ellipsis,
@@ -14,6 +14,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:toastification/toastification.dart';
import 'member_list.dart';
@@ -10,7 +10,6 @@ class MobileQuickActionButton extends StatelessWidget {
required this.text,
this.textColor,
this.iconColor,
this.iconSize,
this.enable = true,
});
@@ -19,12 +18,10 @@ class MobileQuickActionButton extends StatelessWidget {
final String text;
final Color? textColor;
final Color? iconColor;
final Size? iconSize;
final bool enable;
@override
Widget build(BuildContext context) {
final iconSize = this.iconSize ?? const Size.square(18);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: InkWell(
@@ -40,10 +37,10 @@ class MobileQuickActionButton extends StatelessWidget {
children: [
FlowySvg(
icon,
size: iconSize,
size: const Size.square(18),
color: enable ? iconColor : Theme.of(context).disabledColor,
),
HSpace(30 - iconSize.width),
const HSpace(12),
Expanded(
child: FlowyText.regular(
text,
@@ -108,7 +108,7 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
cursorColor: Colors.transparent,
cursorWidth: 0,
);
final blockBuilders = buildBlockComponentBuilders(
final blockBuilders = getEditorBuilderMap(
context: context,
editorState: editorState,
styleCustomizer: styleCustomizer,
@@ -33,8 +33,6 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
final ChecklistCellBackendService _checklistCellService;
void Function()? _onCellChangedFn;
int? nextPhantomIndex;
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
@@ -54,23 +52,17 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
const ChecklistCellState(
tasks: [],
percent: 0,
showIncompleteOnly: false,
phantomIndex: null,
newTask: false,
),
);
return;
}
final phantomIndex = state.phantomIndex != null
? nextPhantomIndex ?? state.phantomIndex
: null;
emit(
state.copyWith(
tasks: _makeChecklistSelectOptions(data),
percent: data.percentage,
phantomIndex: phantomIndex,
),
);
nextPhantomIndex = null;
},
updateTaskName: (option, name) {
_updateOption(option, name);
@@ -78,28 +70,16 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
selectTask: (id) async {
await _checklistCellService.select(optionId: id);
},
createNewTask: (name, index) async {
await _createTask(name, index);
createNewTask: (name) async {
final result = await _checklistCellService.create(name: name);
result.fold(
(l) => emit(state.copyWith(newTask: true)),
(err) => Log.error(err),
);
},
deleteTask: (id) async {
await _deleteOption([id]);
},
reorderTask: (fromIndex, toIndex) async {
await _reorderTask(fromIndex, toIndex, emit);
},
toggleShowIncompleteOnly: () {
emit(state.copyWith(showIncompleteOnly: !state.showIncompleteOnly));
},
updatePhantomIndex: (index) {
emit(
ChecklistCellState(
tasks: state.tasks,
percent: state.percent,
showIncompleteOnly: state.showIncompleteOnly,
phantomIndex: index,
),
);
},
);
},
);
@@ -115,31 +95,6 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
);
}
Future<void> _createTask(String name, int? index) async {
nextPhantomIndex = index == null ? state.tasks.length + 1 : index + 1;
int? actualIndex = index;
if (index != null && state.showIncompleteOnly) {
int notSelectedTaskCount = 0;
for (int i = 0; i < state.tasks.length; i++) {
if (!state.tasks[i].isSelected) {
notSelectedTaskCount++;
}
if (notSelectedTaskCount == index) {
actualIndex = i + 1;
break;
}
}
}
final result = await _checklistCellService.create(
name: name,
index: actualIndex,
);
result.fold((l) {}, (err) => Log.error(err));
}
void _updateOption(SelectOptionPB option, String name) async {
final result =
await _checklistCellService.updateName(option: option, name: name);
@@ -150,32 +105,6 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
final result = await _checklistCellService.delete(optionIds: options);
result.fold((l) => null, (err) => Log.error(err));
}
Future<void> _reorderTask(
int fromIndex,
int toIndex,
Emitter<ChecklistCellState> emit,
) async {
if (fromIndex < toIndex) {
toIndex--;
}
final tasks = state.showIncompleteOnly
? state.tasks.where((task) => !task.isSelected).toList()
: state.tasks;
final fromId = tasks[fromIndex].data.id;
final toId = tasks[toIndex].data.id;
final newTasks = [...state.tasks];
newTasks.insert(toIndex, newTasks.removeAt(fromIndex));
emit(state.copyWith(tasks: newTasks));
final result = await _checklistCellService.reorder(
fromTaskId: fromId,
toTaskId: toId,
);
result.fold((l) => null, (err) => Log.error(err));
}
}
@freezed
@@ -188,17 +117,9 @@ class ChecklistCellEvent with _$ChecklistCellEvent {
String name,
) = _UpdateTaskName;
const factory ChecklistCellEvent.selectTask(String taskId) = _SelectTask;
const factory ChecklistCellEvent.createNewTask(
String description, {
int? index,
}) = _CreateNewTask;
const factory ChecklistCellEvent.createNewTask(String description) =
_CreateNewTask;
const factory ChecklistCellEvent.deleteTask(String taskId) = _DeleteTask;
const factory ChecklistCellEvent.reorderTask(int fromIndex, int toIndex) =
_ReorderTask;
const factory ChecklistCellEvent.toggleShowIncompleteOnly() = _IncompleteOnly;
const factory ChecklistCellEvent.updatePhantomIndex(int? index) =
_UpdatePhantomIndex;
}
@freezed
@@ -206,8 +127,7 @@ class ChecklistCellState with _$ChecklistCellState {
const factory ChecklistCellState({
required List<ChecklistSelectOption> tasks,
required double percent,
required bool showIncompleteOnly,
required int? phantomIndex,
required bool newTask,
}) = _ChecklistCellState;
factory ChecklistCellState.initial(ChecklistCellController cellController) {
@@ -216,8 +136,7 @@ class ChecklistCellState with _$ChecklistCellState {
return ChecklistCellState(
tasks: _makeChecklistSelectOptions(cellData),
percent: cellData?.percentage ?? 0,
showIncompleteOnly: false,
phantomIndex: null,
newTask: false,
);
}
}
@@ -1,7 +1,7 @@
import 'dart:collection';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -18,7 +18,7 @@ typedef OnFiltersChanged = void Function(List<DatabaseFilter>);
typedef OnSortsChanged = void Function(List<DatabaseSort>);
typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<InsertedRowPB> rows);
typedef OnRowsCreated = void Function(List<RowId> rowIds);
typedef OnRowsUpdated = void Function(
List<RowId> rowIds,
ChangedReason reason,
@@ -29,9 +29,6 @@ typedef OnNumOfRowsChanged = void Function(
UnmodifiableMapView<RowId, RowInfo> rowById,
ChangedReason reason,
);
typedef OnRowsVisibilityChanged = void Function(
List<(RowId, bool)> rowVisibilityChanges,
);
@freezed
class LoadingState with _$LoadingState {
@@ -174,8 +174,7 @@ class FieldController {
(FilterChangesetNotificationPB changeset) {
_filterNotifier?.filters =
_filterListFromPBs(changeset.filters.items);
_fieldNotifier.fieldInfos =
_updateFieldInfos(_fieldNotifier.fieldInfos);
_updateFieldInfos();
},
(err) => Log.error(err),
);
@@ -397,7 +396,7 @@ class FieldController {
(updatedFields, fieldInfos) =
await updateFields(changeset.updatedFields, fieldInfos);
_fieldNotifier.fieldInfos = _updateFieldInfos(fieldInfos);
_fieldNotifier.fieldInfos = fieldInfos;
for (final listener in _updatedFieldCallbacks.values) {
listener(updatedFields);
}
@@ -466,22 +465,25 @@ class FieldController {
_fieldSettings.clear();
_fieldSettings.addAll(setting.fieldSettings.items);
_fieldNotifier.fieldInfos = _updateFieldInfos(_fieldNotifier.fieldInfos);
_updateFieldInfos();
}
/// Attach sort, filter, group information and field settings to `FieldInfo`
List<FieldInfo> _updateFieldInfos(List<FieldInfo> fieldInfos) {
return fieldInfos
.map(
(field) => field.copyWith(
fieldSettings: _fieldSettings
.firstWhereOrNull((setting) => setting.fieldId == field.id),
isGroupField: _groupConfigurationByFieldId[field.id] != null,
hasFilter: getFilterByFieldId(field.id) != null,
hasSort: getSortByFieldId(field.id) != null,
),
)
.toList();
void _updateFieldInfos() {
final List<FieldInfo> newFieldInfos = [];
for (final field in _fieldNotifier.fieldInfos) {
newFieldInfos.add(
field.copyWith(
fieldSettings: _fieldSettings
.firstWhereOrNull((setting) => setting.fieldId == field.id),
isGroupField: _groupConfigurationByFieldId[field.id] != null,
hasFilter: getFilterByFieldId(field.id) != null,
hasSort: getSortByFieldId(field.id) != null,
),
);
}
_fieldNotifier.fieldInfos = newFieldInfos;
}
/// Load all of the fields. This is required when opening the database
@@ -504,8 +506,7 @@ class FieldController {
_loadAllFieldSettings(),
_loadSettings(),
]);
_fieldNotifier.fieldInfos =
_updateFieldInfos(_fieldNotifier.fieldInfos);
_updateFieldInfos();
return FlowyResult.success(null);
},
@@ -14,12 +14,11 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/util/int64_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
abstract class DatabaseFilter extends Equatable {
abstract class DatabaseFilter {
const DatabaseFilter({
required this.filterId,
required this.fieldId,
@@ -113,18 +112,16 @@ abstract class DatabaseFilter extends Equatable {
}
final class TextFilter extends DatabaseFilter {
TextFilter({
const TextFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
required String content,
}) {
this.content = canAttachContent ? content : "";
}
required this.content,
});
final TextFilterConditionPB condition;
late final String content;
final String content;
@override
String get conditionName => condition.filterName;
@@ -185,24 +182,19 @@ final class TextFilter extends DatabaseFilter {
content: content ?? this.content,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition, content];
}
final class NumberFilter extends DatabaseFilter {
NumberFilter({
const NumberFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
required String content,
}) {
this.content = canAttachContent ? content : "";
}
required this.content,
});
final NumberFilterConditionPB condition;
late final String content;
final String content;
@override
String get conditionName => condition.filterName;
@@ -261,9 +253,6 @@ final class NumberFilter extends DatabaseFilter {
content: content ?? this.content,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition, content];
}
final class CheckboxFilter extends DatabaseFilter {
@@ -300,9 +289,6 @@ final class CheckboxFilter extends DatabaseFilter {
condition: condition ?? this.condition,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition];
}
final class ChecklistFilter extends DatabaseFilter {
@@ -339,26 +325,19 @@ final class ChecklistFilter extends DatabaseFilter {
condition: condition ?? this.condition,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition];
}
final class SelectOptionFilter extends DatabaseFilter {
SelectOptionFilter({
const SelectOptionFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
required List<String> optionIds,
}) {
if (canAttachContent) {
this.optionIds.addAll(optionIds);
}
}
required this.optionIds,
});
final SelectOptionFilterConditionPB condition;
final List<String> optionIds = [];
final List<String> optionIds;
@override
String get conditionName => condition.i18n;
@@ -370,7 +349,7 @@ final class SelectOptionFilter extends DatabaseFilter {
@override
String getContentDescription(FieldInfo field) {
if (!canAttachContent || optionIds.isEmpty) {
if (!canAttachContent) {
return condition.i18n;
}
@@ -429,6 +408,13 @@ final class SelectOptionFilter extends DatabaseFilter {
SelectOptionFilterConditionPB? condition,
List<String>? optionIds,
}) {
final options = optionIds ?? this.optionIds;
if (fieldType == FieldType.SingleSelect &&
(condition == SelectOptionFilterConditionPB.OptionIs ||
condition == SelectOptionFilterConditionPB.OptionIsNot) &&
options.length > 1) {
options.removeRange(1, options.length);
}
return SelectOptionFilter(
filterId: filterId,
fieldId: fieldId,
@@ -442,9 +428,6 @@ final class SelectOptionFilter extends DatabaseFilter {
field.fieldType == FieldType.SingleSelect
? const SingleSelectOptionFilterDelegateImpl()
: const MultiSelectOptionFilterDelegateImpl();
@override
List<Object?> get props => [filterId, fieldId, condition, optionIds];
}
enum DateTimeFilterCondition {
@@ -508,8 +491,7 @@ enum DateTimeFilterCondition {
}
static List<DateTimeFilterCondition> availableConditionsForFieldType(
FieldType fieldType,
) {
FieldType fieldType,) {
final result = [...values];
if (fieldType == FieldType.CreatedTime ||
fieldType == FieldType.LastEditedTime) {
@@ -522,37 +504,20 @@ enum DateTimeFilterCondition {
}
final class DateTimeFilter extends DatabaseFilter {
DateTimeFilter({
const DateTimeFilter({
required super.filterId,
required super.fieldId,
required super.fieldType,
required this.condition,
DateTime? timestamp,
DateTime? start,
DateTime? end,
}) {
if (canAttachContent) {
if (condition == DateFilterConditionPB.DateStartsBetween ||
condition == DateFilterConditionPB.DateEndsBetween) {
this.start = start;
this.end = end;
this.timestamp = null;
} else {
this.timestamp = timestamp;
this.start = null;
this.end = null;
}
} else {
this.timestamp = null;
this.start = null;
this.end = null;
}
}
this.timestamp,
this.start,
this.end,
});
final DateFilterConditionPB condition;
late final DateTime? timestamp;
late final DateTime? start;
late final DateTime? end;
final DateTime? timestamp;
final DateTime? start;
final DateTime? end;
@override
String get conditionName => condition.toCondition().filterName;
@@ -689,10 +654,6 @@ final class DateTimeFilter extends DatabaseFilter {
timestamp: timestamp,
);
}
@override
List<Object?> get props =>
[filterId, fieldId, condition, timestamp, start, end];
}
final class TimeFilter extends DatabaseFilter {
@@ -742,7 +703,4 @@ final class TimeFilter extends DatabaseFilter {
content: content ?? this.content,
);
}
@override
List<Object?> get props => [filterId, fieldId, condition, content];
}
@@ -93,9 +93,12 @@ class RowCache {
}
void setRowMeta(RowMetaPB rowMeta) {
final rowInfo = _rowList.get(rowMeta.id);
var rowInfo = _rowList.get(rowMeta.id);
if (rowInfo != null) {
rowInfo.updateRowMeta(rowMeta);
} else {
rowInfo = buildGridRow(rowMeta);
_rowList.add(rowInfo);
}
_changedNotifier?.receive(const ChangedReason.didFetchRow());
@@ -119,9 +122,6 @@ class RowCache {
if (_isInitialRows) {
_hideRows(changeset.invisibleRows);
_showRows(changeset.visibleRows);
_changedNotifier?.receive(
ChangedReason.updateRowsVisibility(changeset),
);
} else {
_pendingVisibilityChanges.add(changeset);
}
@@ -159,19 +159,13 @@ class RowCache {
}
void _insertRows(List<InsertedRowPB> insertRows) {
final InsertedIndexs insertedIndices = [];
for (final insertedRow in insertRows) {
if (insertedRow.hasIndex()) {
final index = _rowList.insert(
insertedRow.index,
buildGridRow(insertedRow.rowMeta),
);
if (index != null) {
insertedIndices.add(index);
}
final insertedIndex =
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
if (insertedIndex != null) {
_changedNotifier?.receive(ChangedReason.insert(insertedIndex));
}
}
_changedNotifier?.receive(ChangedReason.insert(insertedIndices));
}
void _updateRows(List<UpdatedRowPB> updatedRows) {
@@ -214,7 +208,7 @@ class RowCache {
final insertedIndex =
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
if (insertedIndex != null) {
_changedNotifier?.receive(ChangedReason.insert([insertedIndex]));
_changedNotifier?.receive(ChangedReason.insert(insertedIndex));
}
}
}
@@ -316,7 +310,6 @@ class RowChangesetNotifier extends ChangeNotifier {
initial: (_) {},
reorderRows: (_) => notifyListeners(),
reorderSingleRow: (_) => notifyListeners(),
updateRowsVisibility: (_) => notifyListeners(),
setInitialRows: (_) => notifyListeners(),
didFetchRow: (_) => notifyListeners(),
);
@@ -368,7 +361,7 @@ typedef UpdatedIndexMap = LinkedHashMap<RowId, UpdatedIndex>;
@freezed
class ChangedReason with _$ChangedReason {
const factory ChangedReason.insert(InsertedIndexs items) = _Insert;
const factory ChangedReason.insert(InsertedIndex item) = _Insert;
const factory ChangedReason.delete(DeletedIndex item) = _Delete;
const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;
const factory ChangedReason.fieldDidChange() = _FieldDidChange;
@@ -379,9 +372,6 @@ class ChangedReason with _$ChangedReason {
ReorderSingleRowPB reorderRow,
RowInfo rowInfo,
) = _ReorderSingleRow;
const factory ChangedReason.updateRowsVisibility(
RowsVisibilityChangePB changeset,
) = _UpdateRowsVisibility;
const factory ChangedReason.setInitialRows() = _SetInitialRows;
}
@@ -69,7 +69,11 @@ class DatabaseViewCache {
if (changeset.insertedRows.isNotEmpty) {
for (final callback in _callbacks) {
callback.onRowsCreated?.call(changeset.insertedRows);
callback.onRowsCreated?.call(
changeset.insertedRows
.map((insertedRow) => insertedRow.rowMeta.id)
.toList(),
);
}
}
},
@@ -273,11 +273,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
}
},
openRowDetail: (rowMeta) {
final copyState = state;
emit(BoardState.openRowDetail(rowMeta: rowMeta));
emit(copyState);
},
);
},
);
@@ -482,18 +477,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
add(BoardEvent.didReceiveGroups(groupList));
},
);
final onDatabaseChanged = DatabaseCallbacks(
onRowsCreated: (rows) {
for (final row in rows) {
if (!isClosed && row.isHiddenInView) {
add(BoardEvent.openRowDetail(row.rowMeta));
}
}
},
);
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutSettingsChanged: onLayoutSettingsChanged,
onGroupChanged: onGroupChanged,
);
@@ -596,7 +581,6 @@ class BoardEvent with _$BoardEvent {
GroupedRowId groupedRowId,
bool toPrevious,
) = _MoveGroupToAdjacentGroup;
const factory BoardEvent.openRowDetail(RowMetaPB rowMeta) = _OpenRowDetail;
}
@freezed
@@ -623,10 +607,6 @@ class BoardState with _$BoardState {
required List<GroupedRowId> groupedRowIds,
}) = _BoardSetFocusState;
const factory BoardState.openRowDetail({
required RowMetaPB rowMeta,
}) = _BoardOpenRowDetailState;
factory BoardState.initial(String viewId) => BoardState.ready(
viewId: viewId,
groupIds: [],
@@ -292,14 +292,6 @@ class _BoardContentState extends State<_BoardContent> {
ready: (value) {
widget.onEditStateChanged?.call();
},
openRowDetail: (value) {
_openCard(
context: context,
databaseController:
context.read<BoardBloc>().databaseController,
rowMeta: value.rowMeta,
);
},
orElse: () {},
);
},
@@ -122,7 +122,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
deleteEventIds: deletedRowIds,
),
);
emit(state.copyWith(deleteEventIds: const []));
},
didReceiveEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(
@@ -131,11 +130,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
newEvent: event,
),
);
emit(state.copyWith(newEvent: null));
},
openRowDetail: (row) {
emit(state.copyWith(openRow: row));
emit(state.copyWith(openRow: null));
},
);
},
@@ -237,13 +231,15 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {
final payload = DatabaseViewRowIdPB(viewId: viewId, rowId: rowId);
return DatabaseEventGetCalendarEvent(payload).send().fold(
(eventPB) => _calendarEventDataFromEventPB(eventPB),
(r) {
Log.error(r);
return null;
},
);
return DatabaseEventGetCalendarEvent(payload).send().then((result) {
return result.fold(
(eventPB) => _calendarEventDataFromEventPB(eventPB),
(r) {
Log.error(r);
return null;
},
);
});
}
void _loadAllEvents() async {
@@ -312,18 +308,14 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
for (final fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo,
};
},
onRowsCreated: (rows) async {
onRowsCreated: (rowIds) async {
if (isClosed) {
return;
}
for (final row in rows) {
if (row.isHiddenInView) {
add(CalendarEvent.openRowDetail(row.rowMeta));
} else {
final event = await _loadEvent(row.rowMeta.id);
if (event != null) {
add(CalendarEvent.didReceiveEvent(event));
}
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null && !isClosed) {
add(CalendarEvent.didReceiveEvent(event));
}
}
},
@@ -349,30 +341,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
}
}
},
onNumOfRowsChanged: (rows, rowById, reason) {
reason.maybeWhen(
updateRowsVisibility: (changeset) async {
if (isClosed) {
return;
}
for (final id in changeset.invisibleRows) {
if (_containsEvent(id)) {
add(CalendarEvent.didDeleteEvents([id]));
}
}
for (final row in changeset.visibleRows) {
final id = row.rowMeta.id;
if (!_containsEvent(id)) {
final event = await _loadEvent(id);
if (event != null) {
add(CalendarEvent.didReceiveEvent(event));
}
}
}
},
orElse: () {},
);
},
);
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
@@ -404,10 +372,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
return state.allEvents[index].date.day != event.date.day;
}
bool _containsEvent(String rowId) {
return state.allEvents.any((element) => element.event!.eventId == rowId);
}
Int64 _eventTimestamp(CalendarDayEvent event, DateTime date) {
final time =
event.date.hour * 3600 + event.date.minute * 60 + event.date.second;
@@ -473,8 +437,6 @@ class CalendarEvent with _$CalendarEvent {
const factory CalendarEvent.deleteEvent(String viewId, String rowId) =
_DeleteEvent;
const factory CalendarEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail;
}
@freezed
@@ -489,7 +451,6 @@ class CalendarState with _$CalendarState {
CalendarEventData<CalendarDayEvent>? updateEvent,
required List<String> deleteEventIds,
required CalendarLayoutSettingPB? settings,
required RowMetaPB? openRow,
required LoadingState loadingState,
required FlowyError? noneOrError,
}) = _CalendarState;
@@ -500,7 +461,6 @@ class CalendarState with _$CalendarState {
initialEvents: [],
deleteEventIds: [],
settings: null,
openRow: null,
noneOrError: null,
loadingState: LoadingState.loading(),
);
@@ -42,7 +42,7 @@ class UnscheduleEventsBloc
state.copyWith(
allEvents: events,
unscheduleEvents:
events.where((element) => !element.hasTimestamp()).toList(),
events.where((element) => !element.isScheduled).toList(),
),
);
},
@@ -55,7 +55,7 @@ class UnscheduleEventsBloc
state.copyWith(
allEvents: events,
unscheduleEvents:
events.where((element) => !element.hasTimestamp()).toList(),
events.where((element) => !element.isScheduled).toList(),
),
);
},
@@ -65,7 +65,7 @@ class UnscheduleEventsBloc
state.copyWith(
allEvents: events,
unscheduleEvents:
events.where((element) => !element.hasTimestamp()).toList(),
events.where((element) => !element.isScheduled).toList(),
),
);
},
@@ -104,12 +104,12 @@ class UnscheduleEventsBloc
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
onRowsCreated: (rows) async {
onRowsCreated: (rowIds) async {
if (isClosed) {
return;
}
for (final row in rows) {
final event = await _loadEvent(row.rowMeta.id);
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null && !isClosed) {
add(UnscheduleEventsEvent.didReceiveEvent(event));
}
@@ -1,6 +1,3 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -14,6 +11,7 @@ import 'package:appflowy/plugins/database/calendar/application/unschedule_event_
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
@@ -33,8 +31,6 @@ import 'layout/sizes.dart';
import 'toolbar/calendar_setting_bar.dart';
class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override
Widget content(
BuildContext context,
@@ -56,7 +52,6 @@ class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
return CalendarSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
toggleExtension: _toggleExtension,
);
}
@@ -65,18 +60,7 @@ class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
BuildContext context,
DatabaseController controller,
) {
return DatabaseViewSettingExtension(
key: _makeValueKey(controller),
viewId: controller.viewId,
databaseController: controller,
toggleExtension: _toggleExtension,
);
}
@override
void dispose() {
_toggleExtension.dispose();
super.dispose();
return SizedBox.fromSize();
}
ValueKey _makeValueKey(DatabaseController controller) {
@@ -168,18 +152,6 @@ class _CalendarPageState extends State<CalendarPage> {
}
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.openRow != c.openRow,
listener: (context, state) {
if (state.openRow != null) {
showEventDetails(
context: context,
databaseController: _calendarBloc.databaseController,
rowMeta: state.openRow!,
);
}
},
),
],
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
@@ -379,10 +351,10 @@ class _CalendarPageState extends State<CalendarPage> {
void showEventDetails({
required BuildContext context,
required DatabaseController databaseController,
required RowMetaPB rowMeta,
required CalendarEventPB event,
}) {
final rowController = RowController(
rowMeta: rowMeta,
rowMeta: event.rowMeta,
viewId: databaseController.viewId,
rowCache: databaseController.rowCache,
);
@@ -533,7 +505,7 @@ class UnscheduleEventsList extends StatelessWidget {
} else {
showEventDetails(
context: context,
rowMeta: event.rowMeta,
event: event,
databaseController: databaseController,
);
PopoverContainer.of(context).close();
@@ -335,7 +335,7 @@ Widget _toggleItem({
const Spacer(),
Toggle(
value: value,
onChanged: (value) => onToggle(value),
onChanged: (value) => onToggle(!value),
padding: EdgeInsets.zero,
),
],
@@ -1,51 +1,26 @@
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarSettingBar extends StatelessWidget {
const CalendarSettingBar({
super.key,
required this.databaseController,
required this.toggleExtension,
});
final DatabaseController databaseController;
final ToggleExtensionNotifier toggleExtension;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => FilterEditorBloc(
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
),
child: ValueListenableBuilder<bool>(
valueListenable: databaseController.isLoading,
builder: (context, value, child) {
if (value) {
return const SizedBox.shrink();
}
return SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilterButton(
toggleExtension: toggleExtension,
),
const HSpace(2),
SettingButton(
databaseController: databaseController,
),
],
),
);
},
return SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SettingButton(
databaseController: databaseController,
),
],
),
);
}
@@ -1,5 +1,6 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:protobuf/protobuf.dart';
@@ -17,16 +18,12 @@ class ChecklistCellBackendService {
Future<FlowyResult<void, FlowyError>> create({
required String name,
int? index,
}) {
final insert = ChecklistCellInsertPB()..name = name;
if (index != null) {
insert.index = index;
}
final payload = ChecklistCellDataChangesetPB()
..cellId = _makdeCellId()
..insertTask.add(insert);
final payload = ChecklistCellDataChangesetPB.create()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..insertOptions.add(name);
return DatabaseEventUpdateChecklistCell(payload).send();
}
@@ -34,9 +31,11 @@ class ChecklistCellBackendService {
Future<FlowyResult<void, FlowyError>> delete({
required List<String> optionIds,
}) {
final payload = ChecklistCellDataChangesetPB()
..cellId = _makdeCellId()
..deleteTasks.addAll(optionIds);
final payload = ChecklistCellDataChangesetPB.create()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..deleteOptionIds.addAll(optionIds);
return DatabaseEventUpdateChecklistCell(payload).send();
}
@@ -44,9 +43,11 @@ class ChecklistCellBackendService {
Future<FlowyResult<void, FlowyError>> select({
required String optionId,
}) {
final payload = ChecklistCellDataChangesetPB()
..cellId = _makdeCellId()
..completedTasks.add(optionId);
final payload = ChecklistCellDataChangesetPB.create()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId
..selectedOptionIds.add(optionId);
return DatabaseEventUpdateChecklistCell(payload).send();
}
@@ -59,28 +60,12 @@ class ChecklistCellBackendService {
final newOption = option.rebuild((option) {
option.name = name;
});
final payload = ChecklistCellDataChangesetPB()
..cellId = _makdeCellId()
..updateTasks.add(newOption);
return DatabaseEventUpdateChecklistCell(payload).send();
}
Future<FlowyResult<void, FlowyError>> reorder({
required fromTaskId,
required toTaskId,
}) {
final payload = ChecklistCellDataChangesetPB()
..cellId = _makdeCellId()
..reorder = "$fromTaskId $toTaskId";
return DatabaseEventUpdateChecklistCell(payload).send();
}
CellIdPB _makdeCellId() {
return CellIdPB()
final payload = ChecklistCellDataChangesetPB.create()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
..rowId = rowId
..updateOptions.add(newOption);
return DatabaseEventUpdateChecklistCell(payload).send();
}
}
@@ -117,10 +117,7 @@ class FilterEditorBloc extends Bloc<FilterEditorEvent, FilterEditorState> {
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
final now = DateTime.now();
final timestamp =
DateTime(now.year, now.month, now.day).millisecondsSinceEpoch ~/
1000;
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
return _filterBackendSvc.insertDateFilter(
filterId: filterId,
fieldId: fieldId,
@@ -6,6 +6,12 @@ abstract class SelectOptionFilterDelegate {
const SelectOptionFilterDelegate();
List<SelectOptionPB> getOptions(FieldInfo fieldInfo);
Set<String> selectOption(
List<String> currentOptionIds,
String optionId,
SelectOptionFilterConditionPB condition,
);
}
class SingleSelectOptionFilterDelegateImpl
@@ -17,6 +23,37 @@ class SingleSelectOptionFilterDelegateImpl
final parser = SingleSelectTypeOptionDataParser();
return parser.fromBuffer(fieldInfo.field.typeOptionData).options;
}
@override
Set<String> selectOption(
List<String> currentOptionIds,
String optionId,
SelectOptionFilterConditionPB condition,
) {
final selectOptionIds = Set<String>.from(currentOptionIds);
switch (condition) {
case SelectOptionFilterConditionPB.OptionIs:
case SelectOptionFilterConditionPB.OptionIsNot:
if (selectOptionIds.isNotEmpty) {
selectOptionIds.clear();
}
selectOptionIds.add(optionId);
break;
case SelectOptionFilterConditionPB.OptionContains:
case SelectOptionFilterConditionPB.OptionDoesNotContain:
selectOptionIds.add(optionId);
break;
case SelectOptionFilterConditionPB.OptionIsEmpty ||
SelectOptionFilterConditionPB.OptionIsNotEmpty:
selectOptionIds.clear();
break;
default:
throw UnimplementedError();
}
return selectOptionIds;
}
}
class MultiSelectOptionFilterDelegateImpl
@@ -29,4 +66,12 @@ class MultiSelectOptionFilterDelegateImpl
.fromBuffer(fieldInfo.field.typeOptionData)
.options;
}
@override
Set<String> selectOption(
List<String> currentOptionIds,
String optionId,
SelectOptionFilterConditionPB condition,
) =>
Set<String>.from(currentOptionIds)..add(optionId);
}
@@ -46,14 +46,6 @@ class GridBloc extends Bloc<GridEvent, GridState> {
_startListening();
await _openGrid(emit);
},
openRowDetail: (row) {
emit(
state.copyWith(
createdRow: row,
openRowDetail: true,
),
);
},
createRow: (openRowDetail) async {
final result = await RowBackendService.createRow(viewId: viewId);
result.fold(
@@ -83,6 +75,9 @@ class GridBloc extends Bloc<GridEvent, GridState> {
databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);
},
didReceiveGridUpdate: (grid) {
emit(state.copyWith(grid: grid));
},
didReceiveFieldUpdate: (fields) {
emit(
state.copyWith(
@@ -117,22 +112,20 @@ class GridBloc extends Bloc<GridEvent, GridState> {
);
}
RowCache get rowCache => databaseController.rowCache;
RowCache getRowCache(RowId rowId) => databaseController.rowCache;
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
onDatabaseChanged: (database) {
if (!isClosed) {
add(GridEvent.didReceiveGridUpdate(database));
}
},
onNumOfRowsChanged: (rowInfos, _, reason) {
if (!isClosed) {
add(GridEvent.didLoadRows(rowInfos, reason));
}
},
onRowsCreated: (rows) {
for (final row in rows) {
if (!isClosed && row.isHiddenInView) {
add(GridEvent.openRowDetail(row.rowMeta));
}
}
},
onRowsUpdated: (rows, reason) {
// TODO(nathan): separate different reasons
if (!isClosed) {
@@ -183,7 +176,6 @@ class GridBloc extends Bloc<GridEvent, GridState> {
@freezed
class GridEvent with _$GridEvent {
const factory GridEvent.initial() = InitialGrid;
const factory GridEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail;
const factory GridEvent.createRow({bool? openRowDetail}) = _CreateRow;
const factory GridEvent.resetCreatedRow() = _ResetCreatedRow;
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
@@ -196,6 +188,10 @@ class GridEvent with _$GridEvent {
List<FieldInfo> fields,
) = _DidReceiveFieldUpdate;
const factory GridEvent.didReceiveGridUpdate(
DatabasePB grid,
) = _DidReceiveGridUpdate;
const factory GridEvent.didReceveFilters(List<DatabaseFilter> filters) =
_DidReceiveFilters;
const factory GridEvent.didReceveSorts(List<DatabaseSort> sorts) =
@@ -206,6 +202,7 @@ class GridEvent with _$GridEvent {
class GridState with _$GridState {
const factory GridState({
required String viewId,
required DatabasePB? grid,
required List<FieldInfo> fields,
required List<RowInfo> rowInfos,
required int rowCount,
@@ -223,6 +220,7 @@ class GridState with _$GridState {
rowInfos: [],
rowCount: 0,
createdRow: null,
grid: null,
viewId: viewId,
reorderable: true,
loadingState: const LoadingState.loading(),
@@ -1,11 +1,10 @@
import 'dart:async';
import 'dart:math';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
@@ -19,7 +18,6 @@ import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -145,7 +143,18 @@ class _GridPageState extends State<GridPage> {
}
},
child: BlocConsumer<GridBloc, GridState>(
listener: listener,
listener: (context, state) => state.loadingState.whenOrNull(
// If initial row id is defined, open row details overlay
finish: (_) {
if (widget.initialRowId != null && !_didOpenInitialRow) {
_didOpenInitialRow = true;
_openRow(context, widget.initialRowId!);
}
return;
},
),
builder: (context, state) => state.loadingState.map(
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
@@ -176,7 +185,7 @@ class _GridPageState extends State<GridPage> {
) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final gridBloc = context.read<GridBloc>();
final rowCache = gridBloc.rowCache;
final rowCache = gridBloc.getRowCache(rowId);
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
if (rowMeta == null) {
return;
@@ -201,48 +210,6 @@ class _GridPageState extends State<GridPage> {
);
});
}
void listener(BuildContext context, GridState state) {
state.loadingState.whenOrNull(
// If initial row id is defined, open row details overlay
finish: (_) async {
if (widget.initialRowId != null && !_didOpenInitialRow) {
_didOpenInitialRow = true;
_openRow(context, widget.initialRowId!);
return;
}
final bloc = context.read<DatabaseTabBarBloc>();
final isCurrentView =
bloc.state.tabBars[bloc.state.selectedIndex].viewId ==
widget.view.id;
if (state.openRowDetail && state.createdRow != null && isCurrentView) {
final rowController = RowController(
viewId: widget.view.id,
rowMeta: state.createdRow!,
rowCache: context.read<GridBloc>().rowCache,
);
unawaited(
FlowyOverlay.show(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
databaseController:
context.read<GridBloc>().databaseController,
rowController: rowController,
userProfile: context.read<GridBloc>().userProfile,
),
),
),
);
context.read<GridBloc>().add(const GridEvent.resetCreatedRow());
}
},
);
}
}
class GridPageContent extends StatefulWidget {
@@ -325,41 +292,21 @@ class _GridRows extends StatefulWidget {
class _GridRowsState extends State<_GridRows> {
bool showFloatingCalculations = false;
bool isAtBottom = false;
@override
void initState() {
super.initState();
_evaluateFloatingCalculations();
widget.scrollController.verticalController.addListener(_onScrollChanged);
}
void _onScrollChanged() {
final controller = widget.scrollController.verticalController;
final isAtBottom = controller.position.atEdge && controller.offset > 0 ||
controller.offset >= controller.position.maxScrollExtent - 1;
if (isAtBottom != this.isAtBottom) {
setState(() => this.isAtBottom = isAtBottom);
}
}
@override
void dispose() {
widget.scrollController.verticalController.removeListener(_onScrollChanged);
super.dispose();
}
void _evaluateFloatingCalculations() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
final verticalController = widget.scrollController.verticalController;
// maxScrollExtent is 0.0 if scrolling is not possible
showFloatingCalculations =
verticalController.position.maxScrollExtent > 0;
isAtBottom = verticalController.position.atEdge &&
verticalController.offset > 0;
showFloatingCalculations = widget.scrollController.verticalController
.position.maxScrollExtent >
0;
});
}
});
@@ -409,11 +356,11 @@ class _GridRowsState extends State<_GridRows> {
) {
// 1. GridRowBottomBar
// 2. GridCalculationsRow
final itemCount =
state.rowInfos.length + (showFloatingCalculations ? 1 : 2);
return Column(
// 3. Footer Padding
final itemCount = state.rowInfos.length + 3;
return Stack(
children: [
Expanded(
Positioned.fill(
child: ReorderableListView.builder(
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
@@ -461,31 +408,38 @@ class _GridRowsState extends State<_GridRows> {
},
itemCount: itemCount,
itemBuilder: (context, index) {
if (index < state.rowInfos.length) {
return _renderRow(
context,
state.rowInfos[index].rowId,
index: index,
);
}
if (index == state.rowInfos.length) {
return const GridRowBottomBar(key: Key('grid_footer'));
}
if (index == state.rowInfos.length + 1 &&
!showFloatingCalculations) {
return GridCalculationsRow(
key: const Key('grid_calculations'),
viewId: widget.viewId,
);
if (index == state.rowInfos.length + 1) {
if (showFloatingCalculations) {
return const SizedBox(
key: Key('calculations_bottom_padding'),
height: 36,
);
} else {
return GridCalculationsRow(
key: const Key('grid_calculations'),
viewId: widget.viewId,
);
}
}
return _renderRow(
context,
state.rowInfos[index].rowId,
index: index,
);
return const SizedBox(key: Key('footer_padding'), height: 10);
},
),
),
if (showFloatingCalculations) ...[
_PositionedCalculationsRow(
viewId: widget.viewId,
isAtBottom: isAtBottom,
),
_PositionedCalculationsRow(viewId: widget.viewId),
],
],
);
@@ -508,7 +462,7 @@ class _GridRowsState extends State<_GridRows> {
}
final child = GridRow(
key: ValueKey("grid_row_$rowId"),
key: ValueKey(rowId),
fieldController: databaseController.fieldController,
rowId: rowId,
viewId: viewId,
@@ -588,16 +542,10 @@ class _WrapScrollView extends StatelessWidget {
class _PositionedCalculationsRow extends StatefulWidget {
const _PositionedCalculationsRow({
required this.viewId,
this.isAtBottom = false,
});
final String viewId;
/// We don't need to show the top border if the scroll offset
/// is at the bottom of the ScrollView.
///
final bool isAtBottom;
@override
State<_PositionedCalculationsRow> createState() =>
_PositionedCalculationsRowState();
@@ -607,28 +555,32 @@ class _PositionedCalculationsRowState
extends State<_PositionedCalculationsRow> {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(
left: context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
),
padding: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
border: widget.isAtBottom
? null
: Border(
top: BorderSide(
color: AFThemeExtension.of(context).borderColor,
),
),
),
child: SizedBox(
height: 36,
width: double.infinity,
child: GridCalculationsRow(
key: const Key('floating_grid_calculations'),
viewId: widget.viewId,
includeDefaultInsets: false,
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
margin: EdgeInsets.only(
left:
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
),
padding: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
border: Border(
top: BorderSide(
color: AFThemeExtension.of(context).borderColor,
),
),
),
child: SizedBox(
height: 36,
width: double.infinity,
child: GridCalculationsRow(
key: const Key('floating_grid_calculations'),
viewId: widget.viewId,
includeDefaultInsets: false,
),
),
),
);
@@ -25,14 +25,12 @@ class SelectOptionFilterConditionList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final conditions = (fieldType == FieldType.SingleSelect
? SingleSelectOptionFilterCondition().conditions
: MultiSelectOptionFilterCondition().conditions);
return PopoverActionList<ConditionWrapper>(
asBarrier: true,
mutex: popoverMutex,
direction: PopoverDirection.bottomWithCenterAligned,
actions: conditions
actions: SingleSelectOptionFilterCondition()
.conditions
.map(
(action) => ConditionWrapper(
action.$1,
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/filter/select_option_loader.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
@@ -16,12 +17,14 @@ class SelectOptionFilterList extends StatelessWidget {
super.key,
required this.filter,
required this.field,
required this.delegate,
required this.options,
required this.onTap,
});
final SelectOptionFilter filter;
final FieldInfo field;
final SelectOptionFilterDelegate delegate;
final List<SelectOptionPB> options;
final VoidCallback onTap;
@@ -50,13 +53,20 @@ class SelectOptionFilterList extends StatelessWidget {
SelectOptionPB option,
bool isSelected,
) {
final selectedOptionIds = Set<String>.from(filter.optionIds);
if (isSelected) {
selectedOptionIds.remove(option.id);
final selectedOptionIds = Set<String>.from(filter.optionIds)
..remove(option.id);
_updateSelectOptions(context, filter, selectedOptionIds);
} else {
selectedOptionIds.add(option.id);
final selectedOptionIds = delegate.selectOption(
filter.optionIds,
option.id,
filter.condition,
);
_updateSelectOptions(context, filter, selectedOptionIds);
}
_updateSelectOptions(context, filter, selectedOptionIds);
onTap();
}
@@ -1,6 +1,7 @@
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -75,7 +76,11 @@ class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
SliverToBoxAdapter(child: _buildFilterPanel(filter, field)),
];
if (filter.canAttachContent) {
if (![
SelectOptionFilterConditionPB.OptionIsEmpty,
SelectOptionFilterConditionPB.OptionIsNotEmpty,
].contains(filter.condition)) {
final delegate = filter.makeDelegate(field);
slivers
..add(const SliverToBoxAdapter(child: VSpace(4)))
..add(
@@ -83,7 +88,8 @@ class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
child: SelectOptionFilterList(
filter: filter,
field: field,
options: filter.makeDelegate(field).getOptions(field),
delegate: delegate,
options: delegate.getOptions(field),
onTap: () => popoverMutex.close(),
),
),
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
@@ -9,6 +7,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class GridAddRowButton extends StatelessWidget {
@@ -44,9 +44,7 @@ class MobileTabBarHeader extends StatelessWidget {
controller: state
.tabBarControllerByViewId[currentView.viewId]!.controller,
features: switch (currentView.layout) {
ViewLayoutPB.Board || ViewLayoutPB.Calendar => [
MobileDatabaseControlFeatures.filter,
],
ViewLayoutPB.Board => [MobileDatabaseControlFeatures.filter],
ViewLayoutPB.Grid => [
MobileDatabaseControlFeatures.sort,
MobileDatabaseControlFeatures.filter,
@@ -17,6 +17,7 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
BuildContext context,
CellContainerNotifier cellContainerNotifier,
ChecklistCellBloc bloc,
ChecklistCellState state,
PopoverController popoverController,
) {
return AppFlowyPopover(
@@ -26,7 +27,7 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
skipTraversal: true,
popupBuilder: (popoverContext) {
popupBuilder: (BuildContext popoverContext) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cellContainerNotifier.isFocus = true;
});
@@ -38,19 +39,15 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
);
},
onClose: () => cellContainerNotifier.isFocus = false,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: state.tasks.isEmpty
? const SizedBox.shrink()
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
);
},
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: state.tasks.isEmpty
? const SizedBox.shrink()
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
),
);
}
Loaded 100 of 578 files, more files were not shown because too many files have changed in this diff. Show more