Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
251b16cb62 | ||
|
|
da80c599bf | ||
|
|
ca6a900874 | ||
|
|
5e09e96f7b | ||
|
|
068500df84 | ||
|
|
15949a7e21 | ||
|
|
e635fe3378 | ||
|
|
8276c5db57 | ||
|
|
1d9876a6eb | ||
|
|
6340174927 | ||
|
|
7b031b228e | ||
|
|
ec408940e4 | ||
|
|
bd46fc11f4 | ||
|
|
42e55fe248 | ||
|
|
3a9011b3cb | ||
|
|
e3bf0442e8 | ||
|
|
0413100e2b | ||
|
|
7cad04bbf4 | ||
|
|
c9f0bbf26a | ||
|
|
d91639172a | ||
|
|
5f0cc1becd | ||
|
|
a8bcab7770 | ||
|
|
b1682e4f54 | ||
|
|
2447b34107 | ||
|
|
8126d19682 | ||
|
|
6cc8c81e3d | ||
|
|
e86d812ee6 | ||
|
|
c6f042830f | ||
|
|
2378c0c441 | ||
|
|
7bc53d7bc6 | ||
|
|
7918d6c0d1 | ||
|
|
073e8b897b | ||
|
|
46a3006f4d | ||
|
|
5d578ab9e6 | ||
|
|
f3fe729900 | ||
|
|
a04c157770 | ||
|
|
320ffcb434 | ||
|
|
37a2fb17b5 | ||
|
|
b5936cec54 | ||
|
|
ea61c81cce | ||
|
|
64d71d0d20 | ||
|
|
af08b4c004 | ||
|
|
ea670b6ae6 | ||
|
|
f19e354418 | ||
|
|
b54e3dd243 | ||
|
|
9798e39e49 | ||
|
|
66fd0c19da | ||
|
|
8d6c67206c | ||
|
|
8cf683eb50 | ||
|
|
580a23f3f5 | ||
|
|
29dc46a993 | ||
|
|
93885bad7b | ||
|
|
6e0619c21e |
No files matched your search
@@ -28,6 +28,7 @@ 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 }}
|
||||
@@ -73,9 +74,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [windows-2019]
|
||||
os: [windows-latest]
|
||||
include:
|
||||
- os: windows-2019
|
||||
- os: windows-latest
|
||||
flutter_profile: development-windows-x86
|
||||
target: x86_64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -247,9 +248,9 @@ jobs:
|
||||
- name: Run Docker-Compose
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
|
||||
@@ -22,6 +22,7 @@ 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 }}
|
||||
@@ -48,13 +49,13 @@ jobs:
|
||||
model: "iPhone 15"
|
||||
shutdown_after_job: false
|
||||
|
||||
build-macos:
|
||||
integration-tests:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: macos-13
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -96,11 +97,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
|
||||
@@ -111,7 +112,6 @@ 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 }}
|
||||
@@ -20,6 +20,7 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CLOUD_VERSION: 0.6.34-amd64
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
|
||||
jobs:
|
||||
@@ -46,9 +47,9 @@ jobs:
|
||||
- name: Ensure AppFlowy-Cloud is Running with Correct Version
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
@@ -93,6 +94,11 @@ 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
|
||||
@@ -135,9 +141,9 @@ jobs:
|
||||
- name: Ensure AppFlowy-Cloud is Running with Correct Version
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
APPFLOWY_CLOUD_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_HISTORY_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_WORKER_VERSION: 0.6.29-amd64
|
||||
APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
|
||||
run: |
|
||||
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
|
||||
if [ -z "$container_id" ]; then
|
||||
|
||||
+16
-1
@@ -1,4 +1,19 @@
|
||||
# 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
|
||||
@@ -60,7 +75,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Acknowledgements
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to these amazing projects which help power AppFlowy.IO:
|
||||
|
||||
|
||||
@@ -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.1"
|
||||
APPFLOWY_VERSION = "0.7.2"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
# 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:
|
||||
@@ -16,17 +6,6 @@ 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
|
||||
|
||||
@@ -51,8 +30,5 @@ 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
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
-32
@@ -68,38 +68,6 @@ 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,
|
||||
|
||||
+3
@@ -1,9 +1,12 @@
|
||||
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();
|
||||
}
|
||||
+9
@@ -18,6 +18,7 @@ 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';
|
||||
@@ -92,6 +93,14 @@ 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,
|
||||
|
||||
+68
-1
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -277,5 +277,72 @@ 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+27
-2
@@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -316,6 +317,30 @@ 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 {
|
||||
|
||||
+35
-10
@@ -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(1);
|
||||
tester.assertNumberOfRowsInGridPage(2);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
@@ -154,12 +154,10 @@ void main() {
|
||||
tester.assertNumberOfRowsInGridPage(0);
|
||||
|
||||
await tester.tapFilterButtonInGrid('date');
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
|
||||
await tester.changeDateFilterCondition(DateTimeFilterCondition.before);
|
||||
tester.assertNumberOfRowsInGridPage(7);
|
||||
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateTimeFilterCondition.isEmpty);
|
||||
await tester.changeDateFilterCondition(DateTimeFilterCondition.isEmpty);
|
||||
tester.assertNumberOfRowsInGridPage(3);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
@@ -171,8 +169,6 @@ void main() {
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
|
||||
await tester.createField(
|
||||
FieldType.CreatedTime,
|
||||
name: 'Created at',
|
||||
@@ -189,11 +185,40 @@ void main() {
|
||||
tester.assertNumberOfRowsInGridPage(3);
|
||||
|
||||
await tester.tapFilterButtonInGrid('Created at');
|
||||
await tester.tapDateFilterButtonInGrid();
|
||||
await tester.tapDateFilterCondition(DateTimeFilterCondition.before);
|
||||
await tester.changeDateFilterCondition(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();
|
||||
});
|
||||
});
|
||||
}
|
||||
+138
-1
@@ -1,3 +1,6 @@
|
||||
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';
|
||||
@@ -9,7 +12,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:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -335,5 +338,139 @@ 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
+139
-3
@@ -1,3 +1,8 @@
|
||||
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';
|
||||
|
||||
@@ -7,9 +12,23 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// +, ... button beside the block component.
|
||||
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',
|
||||
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''',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -40,5 +59,122 @@ 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])));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
-3
@@ -8,7 +8,6 @@ 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;
|
||||
@@ -40,13 +39,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
|
||||
}
|
||||
+9
@@ -1,7 +1,12 @@
|
||||
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();
|
||||
@@ -11,4 +16,8 @@ void 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();
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -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(AppFlowyErrorPage), findsOneWidget);
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+3
-10
@@ -1,9 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -182,13 +179,9 @@ Future<void> hoverAndClickDepthOptionAction(
|
||||
List<int> path,
|
||||
int level,
|
||||
) async {
|
||||
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.editor.openDepthMenu(path);
|
||||
final type = OptionDepthType.fromLevel(level);
|
||||
await tester.tapButton(find.findTextInFlowyText(type.description));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
|
||||
@@ -189,15 +189,46 @@ void main() {
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
await tester.hoverOnFirstRowOfGrid(() async {
|
||||
// Open the row menu and then click the delete
|
||||
// Open the row menu and click the delete button
|
||||
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,6 +52,7 @@ 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(ViewMoreActionButton), findsOneWidget);
|
||||
expect(find.byType(ViewMoreActionPopover), findsOneWidget);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
||||
import 'sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar_test.dart' as sidebar_test;
|
||||
import 'sidebar_view_item_test.dart' as sidebar_view_item_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -12,4 +13,5 @@ void main() {
|
||||
// sidebar_expanded_test.main();
|
||||
sidebar_favorite_test.main();
|
||||
sidebar_icon_test.main();
|
||||
sidebar_view_item_test.main();
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
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,5 +1,9 @@
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -57,10 +57,11 @@ void main() {
|
||||
.single
|
||||
.widget as AppFlowyEditorPage;
|
||||
return editorPage.styleCustomizer
|
||||
.style()
|
||||
.textStyleConfiguration
|
||||
.text
|
||||
.height!;
|
||||
.style()
|
||||
.textStyleConfiguration
|
||||
.text
|
||||
.height ??
|
||||
PageStyleLineHeightLayout.normal.lineHeight;
|
||||
}
|
||||
|
||||
testWidgets('change font size in page style settings', (tester) async {
|
||||
@@ -126,7 +127,6 @@ 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(
|
||||
|
||||
+7
-7
@@ -3,10 +3,11 @@
|
||||
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';
|
||||
@@ -34,13 +35,12 @@ 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.byKey(mobileCreateNewPageButtonKey);
|
||||
final createPageButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowySvg &&
|
||||
widget.svg.path == FlowySvgs.m_home_unselected_m.path,
|
||||
);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -29,11 +29,6 @@ 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,3 +1,4 @@
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'mobile/document/page_style_test.dart' as page_style_test;
|
||||
@@ -5,7 +6,9 @@ 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();
|
||||
|
||||
@@ -17,6 +17,7 @@ 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});
|
||||
@@ -105,7 +106,7 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> waitUntilSignInPageShow() async {
|
||||
if (isAuthEnabled) {
|
||||
if (isAuthEnabled || UniversalPlatform.isMobile) {
|
||||
final finder = find.byType(SignInAnonymousButtonV2);
|
||||
await pumpUntilFound(finder, timeout: const Duration(seconds: 30));
|
||||
expect(finder, findsOneWidget);
|
||||
@@ -158,17 +159,12 @@ 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(
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -42,6 +43,7 @@ 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';
|
||||
|
||||
@@ -185,6 +187,16 @@ 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, {
|
||||
@@ -199,8 +211,11 @@ extension CommonOperations on WidgetTester {
|
||||
///
|
||||
/// Must call [hoverOnPageName] first.
|
||||
Future<void> tapPageOptionButton() async {
|
||||
final optionButton = find.byType(ViewMoreActionButton);
|
||||
await tapButton(optionButton);
|
||||
final optionButton = find.descendant(
|
||||
of: find.byType(ViewMoreActionPopover),
|
||||
matching: find.byFlowySvg(FlowySvgs.workspace_three_dots_s),
|
||||
);
|
||||
await tapButton(optionButton, warnIfMissed: true);
|
||||
}
|
||||
|
||||
/// Tap the delete page button.
|
||||
@@ -279,12 +294,14 @@ extension CommonOperations on WidgetTester {
|
||||
|
||||
/// Tap the delete permanently button.
|
||||
///
|
||||
/// the restore button will show after the current page is deleted.
|
||||
/// the delete permanently button will show after the current page is deleted.
|
||||
Future<void> tapDeletePermanentlyButton() async {
|
||||
final restoreButton = find.textContaining(
|
||||
final deleteButton = find.textContaining(
|
||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
||||
);
|
||||
await tapButton(restoreButton);
|
||||
await tapButton(deleteButton);
|
||||
await tap(find.text(LocaleKeys.button_delete.tr()));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Tap the share button above the document page.
|
||||
@@ -348,6 +365,32 @@ extension CommonOperations on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +1,8 @@
|
||||
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';
|
||||
@@ -411,8 +413,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> selectOption({required String name}) async {
|
||||
final option = find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
|
||||
final option = find.descendant(
|
||||
of: find.byType(SelectOptionCellEditor),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(option);
|
||||
@@ -504,6 +509,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> renameChecklistTask({
|
||||
required int index,
|
||||
required String name,
|
||||
bool enter = true,
|
||||
}) async {
|
||||
final textField = find
|
||||
.descendant(
|
||||
@@ -513,7 +519,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
.at(index);
|
||||
|
||||
await enterText(textField, name);
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
if (enter) {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -531,14 +539,32 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> deleteChecklistTask({required int index}) async {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
|
||||
await pumpAndSettle();
|
||||
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
|
||||
await hoverOnWidget(
|
||||
task,
|
||||
onHover: () async {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
|
||||
);
|
||||
await tapButton(button);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await tapButton(button);
|
||||
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 phantom = widget<Padding>(paddings.at(index)).child!;
|
||||
expect(phantom is PhantomChecklistItem, true);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
@@ -1157,10 +1183,6 @@ 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(
|
||||
@@ -1194,7 +1216,36 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapDateFilterCondition(DateTimeFilterCondition condition) async {
|
||||
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));
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(condition.filterName),
|
||||
|
||||
@@ -2,10 +2,13 @@ import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
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';
|
||||
@@ -15,6 +18,7 @@ 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';
|
||||
@@ -71,6 +75,20 @@ 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(
|
||||
@@ -179,6 +197,11 @@ 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.
|
||||
@@ -275,10 +298,42 @@ 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.
|
||||
@@ -360,4 +415,23 @@ 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();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ 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();
|
||||
}
|
||||
+2
-2
@@ -56,7 +56,7 @@ class NotificationReminderBloc
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
createdAt: createdAt,
|
||||
pageTitle: view.name,
|
||||
pageTitle: view.nameOrDefault,
|
||||
view: view,
|
||||
reminderContent: node.delta?.toPlainText() ?? '',
|
||||
nodes: [node],
|
||||
@@ -68,7 +68,7 @@ class NotificationReminderBloc
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
createdAt: createdAt,
|
||||
pageTitle: view.name,
|
||||
pageTitle: view.nameOrDefault,
|
||||
view: view,
|
||||
reminderContent: reminder.message,
|
||||
status: NotificationReminderStatus.loaded,
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -94,6 +95,10 @@ 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,6 +8,7 @@ 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';
|
||||
@@ -98,6 +99,8 @@ 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),
|
||||
),
|
||||
|
||||
+127
-15
@@ -1,11 +1,24 @@
|
||||
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});
|
||||
@@ -16,29 +29,20 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ViewPageBottomSheet(
|
||||
view: view,
|
||||
onAction: (action) {
|
||||
onAction: (action) async {
|
||||
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.pop();
|
||||
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
|
||||
|
||||
context.pop();
|
||||
break;
|
||||
case MobileViewBottomSheetBodyAction.undo:
|
||||
EditorNotification.undo().post();
|
||||
@@ -52,17 +56,125 @@ 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) {
|
||||
if (name != view.name) {
|
||||
context.read<ViewBloc>().add(ViewEvent.rename(name));
|
||||
}
|
||||
_onRename(context, 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
-2
@@ -1,21 +1,29 @@
|
||||
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;
|
||||
helpCenter,
|
||||
publish,
|
||||
unpublish,
|
||||
copyPublishLink,
|
||||
visitSite,
|
||||
copyShareLink,
|
||||
}
|
||||
|
||||
typedef MobileViewBottomSheetBodyActionCallback = void Function(
|
||||
@@ -90,6 +98,7 @@ 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,
|
||||
),
|
||||
@@ -100,6 +109,7 @@ 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
|
||||
@@ -110,16 +120,30 @@ 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,
|
||||
),
|
||||
@@ -129,6 +153,48 @@ 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,
|
||||
|
||||
+5
-21
@@ -1003,7 +1003,7 @@ class _SelectOptionFilterContentEditorState
|
||||
isSelected,
|
||||
);
|
||||
},
|
||||
indicator: _getIndicator(),
|
||||
indicator: MobileSelectedOptionIndicator.multi,
|
||||
showMoreOptionsButton: false,
|
||||
);
|
||||
},
|
||||
@@ -1027,35 +1027,19 @@ 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) {
|
||||
final selectedOptionIds = Set<String>.from(widget.filter.optionIds)
|
||||
..remove(option.id);
|
||||
|
||||
_updateSelectOptions(context, options, selectedOptionIds);
|
||||
selectedOptionIds.remove(option.id);
|
||||
} else {
|
||||
final selectedOptionIds = widget.delegate.selectOption(
|
||||
widget.filter.optionIds,
|
||||
option.id,
|
||||
widget.filter.condition,
|
||||
);
|
||||
|
||||
_updateSelectOptions(context, options, selectedOptionIds);
|
||||
selectedOptionIds.add(option.id);
|
||||
}
|
||||
_updateSelectOptions(context, options, selectedOptionIds);
|
||||
}
|
||||
|
||||
void _updateSelectOptions(
|
||||
|
||||
-4
@@ -93,8 +93,6 @@ final class SingleSelectOptionFilterCondition
|
||||
return [
|
||||
SelectOptionFilterConditionPB.OptionIs,
|
||||
SelectOptionFilterConditionPB.OptionIsNot,
|
||||
SelectOptionFilterConditionPB.OptionContains,
|
||||
SelectOptionFilterConditionPB.OptionDoesNotContain,
|
||||
SelectOptionFilterConditionPB.OptionIsEmpty,
|
||||
SelectOptionFilterConditionPB.OptionIsNotEmpty,
|
||||
].map((e) => (e, e.i18n)).toList();
|
||||
@@ -109,8 +107,6 @@ final class MultiSelectOptionFilterCondition
|
||||
return [
|
||||
SelectOptionFilterConditionPB.OptionContains,
|
||||
SelectOptionFilterConditionPB.OptionDoesNotContain,
|
||||
SelectOptionFilterConditionPB.OptionIs,
|
||||
SelectOptionFilterConditionPB.OptionIsNot,
|
||||
SelectOptionFilterConditionPB.OptionIsEmpty,
|
||||
SelectOptionFilterConditionPB.OptionIsNotEmpty,
|
||||
].map((e) => (e, e.i18n)).toList();
|
||||
|
||||
@@ -26,7 +26,6 @@ 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});
|
||||
|
||||
@@ -258,7 +258,7 @@ class NotificationDocumentContent extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
final blockBuilders = getEditorBuilderMap(
|
||||
final blockBuilders = buildBlockComponentBuilders(
|
||||
context: context,
|
||||
editorState: editorState,
|
||||
styleCustomizer: styleCustomizer,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
@@ -9,7 +8,6 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -262,9 +260,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
// title
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
widget.view.name.isEmpty
|
||||
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
|
||||
: widget.view.name,
|
||||
widget.view.nameOrDefault,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 20.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ 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';
|
||||
|
||||
|
||||
+5
-2
@@ -10,6 +10,7 @@ class MobileQuickActionButton extends StatelessWidget {
|
||||
required this.text,
|
||||
this.textColor,
|
||||
this.iconColor,
|
||||
this.iconSize,
|
||||
this.enable = true,
|
||||
});
|
||||
|
||||
@@ -18,10 +19,12 @@ 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(
|
||||
@@ -37,10 +40,10 @@ class MobileQuickActionButton extends StatelessWidget {
|
||||
children: [
|
||||
FlowySvg(
|
||||
icon,
|
||||
size: const Size.square(18),
|
||||
size: iconSize,
|
||||
color: enable ? iconColor : Theme.of(context).disabledColor,
|
||||
),
|
||||
const HSpace(12),
|
||||
HSpace(30 - iconSize.width),
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
text,
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
|
||||
cursorColor: Colors.transparent,
|
||||
cursorWidth: 0,
|
||||
);
|
||||
final blockBuilders = getEditorBuilderMap(
|
||||
final blockBuilders = buildBlockComponentBuilders(
|
||||
context: context,
|
||||
editorState: editorState,
|
||||
styleCustomizer: styleCustomizer,
|
||||
|
||||
+92
-11
@@ -33,6 +33,8 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
final ChecklistCellBackendService _checklistCellService;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
int? nextPhantomIndex;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
@@ -52,17 +54,23 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
const ChecklistCellState(
|
||||
tasks: [],
|
||||
percent: 0,
|
||||
newTask: false,
|
||||
showIncompleteOnly: false,
|
||||
phantomIndex: null,
|
||||
),
|
||||
);
|
||||
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);
|
||||
@@ -70,16 +78,28 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
selectTask: (id) async {
|
||||
await _checklistCellService.select(optionId: id);
|
||||
},
|
||||
createNewTask: (name) async {
|
||||
final result = await _checklistCellService.create(name: name);
|
||||
result.fold(
|
||||
(l) => emit(state.copyWith(newTask: true)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
createNewTask: (name, index) async {
|
||||
await _createTask(name, index);
|
||||
},
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -95,6 +115,31 @@ 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);
|
||||
@@ -105,6 +150,32 @@ 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
|
||||
@@ -117,9 +188,17 @@ class ChecklistCellEvent with _$ChecklistCellEvent {
|
||||
String name,
|
||||
) = _UpdateTaskName;
|
||||
const factory ChecklistCellEvent.selectTask(String taskId) = _SelectTask;
|
||||
const factory ChecklistCellEvent.createNewTask(String description) =
|
||||
_CreateNewTask;
|
||||
const factory ChecklistCellEvent.createNewTask(
|
||||
String description, {
|
||||
int? index,
|
||||
}) = _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
|
||||
@@ -127,7 +206,8 @@ class ChecklistCellState with _$ChecklistCellState {
|
||||
const factory ChecklistCellState({
|
||||
required List<ChecklistSelectOption> tasks,
|
||||
required double percent,
|
||||
required bool newTask,
|
||||
required bool showIncompleteOnly,
|
||||
required int? phantomIndex,
|
||||
}) = _ChecklistCellState;
|
||||
|
||||
factory ChecklistCellState.initial(ChecklistCellController cellController) {
|
||||
@@ -136,7 +216,8 @@ class ChecklistCellState with _$ChecklistCellState {
|
||||
return ChecklistCellState(
|
||||
tasks: _makeChecklistSelectOptions(cellData),
|
||||
percent: cellData?.percentage ?? 0,
|
||||
newTask: false,
|
||||
showIncompleteOnly: false,
|
||||
phantomIndex: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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<RowId> rowIds);
|
||||
typedef OnRowsCreated = void Function(List<InsertedRowPB> rows);
|
||||
typedef OnRowsUpdated = void Function(
|
||||
List<RowId> rowIds,
|
||||
ChangedReason reason,
|
||||
@@ -29,6 +29,9 @@ typedef OnNumOfRowsChanged = void Function(
|
||||
UnmodifiableMapView<RowId, RowInfo> rowById,
|
||||
ChangedReason reason,
|
||||
);
|
||||
typedef OnRowsVisibilityChanged = void Function(
|
||||
List<(RowId, bool)> rowVisibilityChanges,
|
||||
);
|
||||
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
|
||||
+2
-16
@@ -353,14 +353,7 @@ final class SelectOptionFilter extends DatabaseFilter {
|
||||
required List<String> optionIds,
|
||||
}) {
|
||||
if (canAttachContent) {
|
||||
if (fieldType == FieldType.SingleSelect &&
|
||||
(condition == SelectOptionFilterConditionPB.OptionIs ||
|
||||
condition == SelectOptionFilterConditionPB.OptionIsNot) &&
|
||||
optionIds.isNotEmpty) {
|
||||
this.optionIds.add(optionIds.first);
|
||||
} else {
|
||||
this.optionIds.addAll(optionIds);
|
||||
}
|
||||
this.optionIds.addAll(optionIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +370,7 @@ final class SelectOptionFilter extends DatabaseFilter {
|
||||
|
||||
@override
|
||||
String getContentDescription(FieldInfo field) {
|
||||
if (!canAttachContent) {
|
||||
if (!canAttachContent || optionIds.isEmpty) {
|
||||
return condition.i18n;
|
||||
}
|
||||
|
||||
@@ -436,13 +429,6 @@ 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,
|
||||
|
||||
@@ -93,12 +93,9 @@ class RowCache {
|
||||
}
|
||||
|
||||
void setRowMeta(RowMetaPB rowMeta) {
|
||||
var rowInfo = _rowList.get(rowMeta.id);
|
||||
final rowInfo = _rowList.get(rowMeta.id);
|
||||
if (rowInfo != null) {
|
||||
rowInfo.updateRowMeta(rowMeta);
|
||||
} else {
|
||||
rowInfo = buildGridRow(rowMeta);
|
||||
_rowList.add(rowInfo);
|
||||
}
|
||||
|
||||
_changedNotifier?.receive(const ChangedReason.didFetchRow());
|
||||
@@ -122,6 +119,9 @@ class RowCache {
|
||||
if (_isInitialRows) {
|
||||
_hideRows(changeset.invisibleRows);
|
||||
_showRows(changeset.visibleRows);
|
||||
_changedNotifier?.receive(
|
||||
ChangedReason.updateRowsVisibility(changeset),
|
||||
);
|
||||
} else {
|
||||
_pendingVisibilityChanges.add(changeset);
|
||||
}
|
||||
@@ -159,13 +159,19 @@ class RowCache {
|
||||
}
|
||||
|
||||
void _insertRows(List<InsertedRowPB> insertRows) {
|
||||
final InsertedIndexs insertedIndices = [];
|
||||
for (final insertedRow in insertRows) {
|
||||
final insertedIndex =
|
||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
|
||||
if (insertedIndex != null) {
|
||||
_changedNotifier?.receive(ChangedReason.insert(insertedIndex));
|
||||
if (insertedRow.hasIndex()) {
|
||||
final index = _rowList.insert(
|
||||
insertedRow.index,
|
||||
buildGridRow(insertedRow.rowMeta),
|
||||
);
|
||||
if (index != null) {
|
||||
insertedIndices.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
_changedNotifier?.receive(ChangedReason.insert(insertedIndices));
|
||||
}
|
||||
|
||||
void _updateRows(List<UpdatedRowPB> updatedRows) {
|
||||
@@ -208,7 +214,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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,6 +316,7 @@ class RowChangesetNotifier extends ChangeNotifier {
|
||||
initial: (_) {},
|
||||
reorderRows: (_) => notifyListeners(),
|
||||
reorderSingleRow: (_) => notifyListeners(),
|
||||
updateRowsVisibility: (_) => notifyListeners(),
|
||||
setInitialRows: (_) => notifyListeners(),
|
||||
didFetchRow: (_) => notifyListeners(),
|
||||
);
|
||||
@@ -361,7 +368,7 @@ typedef UpdatedIndexMap = LinkedHashMap<RowId, UpdatedIndex>;
|
||||
|
||||
@freezed
|
||||
class ChangedReason with _$ChangedReason {
|
||||
const factory ChangedReason.insert(InsertedIndex item) = _Insert;
|
||||
const factory ChangedReason.insert(InsertedIndexs items) = _Insert;
|
||||
const factory ChangedReason.delete(DeletedIndex item) = _Delete;
|
||||
const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;
|
||||
const factory ChangedReason.fieldDidChange() = _FieldDidChange;
|
||||
@@ -372,6 +379,9 @@ class ChangedReason with _$ChangedReason {
|
||||
ReorderSingleRowPB reorderRow,
|
||||
RowInfo rowInfo,
|
||||
) = _ReorderSingleRow;
|
||||
const factory ChangedReason.updateRowsVisibility(
|
||||
RowsVisibilityChangePB changeset,
|
||||
) = _UpdateRowsVisibility;
|
||||
const factory ChangedReason.setInitialRows() = _SetInitialRows;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,11 +69,7 @@ class DatabaseViewCache {
|
||||
|
||||
if (changeset.insertedRows.isNotEmpty) {
|
||||
for (final callback in _callbacks) {
|
||||
callback.onRowsCreated?.call(
|
||||
changeset.insertedRows
|
||||
.map((insertedRow) => insertedRow.rowMeta.id)
|
||||
.toList(),
|
||||
);
|
||||
callback.onRowsCreated?.call(changeset.insertedRows);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -273,6 +273,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
);
|
||||
}
|
||||
},
|
||||
openRowDetail: (rowMeta) {
|
||||
final copyState = state;
|
||||
emit(BoardState.openRowDetail(rowMeta: rowMeta));
|
||||
emit(copyState);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -477,8 +482,18 @@ 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,
|
||||
);
|
||||
@@ -581,6 +596,7 @@ class BoardEvent with _$BoardEvent {
|
||||
GroupedRowId groupedRowId,
|
||||
bool toPrevious,
|
||||
) = _MoveGroupToAdjacentGroup;
|
||||
const factory BoardEvent.openRowDetail(RowMetaPB rowMeta) = _OpenRowDetail;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -607,6 +623,10 @@ 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,6 +292,14 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
ready: (value) {
|
||||
widget.onEditStateChanged?.call();
|
||||
},
|
||||
openRowDetail: (value) {
|
||||
_openCard(
|
||||
context: context,
|
||||
databaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
rowMeta: value.rowMeta,
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
|
||||
+54
-14
@@ -122,6 +122,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
deleteEventIds: deletedRowIds,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(deleteEventIds: const []));
|
||||
},
|
||||
didReceiveEvent: (CalendarEventData<CalendarDayEvent> event) {
|
||||
emit(
|
||||
@@ -130,6 +131,11 @@ 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));
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -231,15 +237,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
|
||||
Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {
|
||||
final payload = DatabaseViewRowIdPB(viewId: viewId, rowId: rowId);
|
||||
return DatabaseEventGetCalendarEvent(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(eventPB) => _calendarEventDataFromEventPB(eventPB),
|
||||
(r) {
|
||||
Log.error(r);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
});
|
||||
return DatabaseEventGetCalendarEvent(payload).send().fold(
|
||||
(eventPB) => _calendarEventDataFromEventPB(eventPB),
|
||||
(r) {
|
||||
Log.error(r);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _loadAllEvents() async {
|
||||
@@ -308,14 +312,18 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
for (final fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo,
|
||||
};
|
||||
},
|
||||
onRowsCreated: (rowIds) async {
|
||||
onRowsCreated: (rows) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
for (final id in rowIds) {
|
||||
final event = await _loadEvent(id);
|
||||
if (event != null && !isClosed) {
|
||||
add(CalendarEvent.didReceiveEvent(event));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -341,6 +349,30 @@ 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(
|
||||
@@ -372,6 +404,10 @@ 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;
|
||||
@@ -437,6 +473,8 @@ class CalendarEvent with _$CalendarEvent {
|
||||
|
||||
const factory CalendarEvent.deleteEvent(String viewId, String rowId) =
|
||||
_DeleteEvent;
|
||||
|
||||
const factory CalendarEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -451,6 +489,7 @@ class CalendarState with _$CalendarState {
|
||||
CalendarEventData<CalendarDayEvent>? updateEvent,
|
||||
required List<String> deleteEventIds,
|
||||
required CalendarLayoutSettingPB? settings,
|
||||
required RowMetaPB? openRow,
|
||||
required LoadingState loadingState,
|
||||
required FlowyError? noneOrError,
|
||||
}) = _CalendarState;
|
||||
@@ -461,6 +500,7 @@ class CalendarState with _$CalendarState {
|
||||
initialEvents: [],
|
||||
deleteEventIds: [],
|
||||
settings: null,
|
||||
openRow: null,
|
||||
noneOrError: null,
|
||||
loadingState: LoadingState.loading(),
|
||||
);
|
||||
|
||||
+6
-6
@@ -42,7 +42,7 @@ class UnscheduleEventsBloc
|
||||
state.copyWith(
|
||||
allEvents: events,
|
||||
unscheduleEvents:
|
||||
events.where((element) => !element.isScheduled).toList(),
|
||||
events.where((element) => !element.hasTimestamp()).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -55,7 +55,7 @@ class UnscheduleEventsBloc
|
||||
state.copyWith(
|
||||
allEvents: events,
|
||||
unscheduleEvents:
|
||||
events.where((element) => !element.isScheduled).toList(),
|
||||
events.where((element) => !element.hasTimestamp()).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -65,7 +65,7 @@ class UnscheduleEventsBloc
|
||||
state.copyWith(
|
||||
allEvents: events,
|
||||
unscheduleEvents:
|
||||
events.where((element) => !element.isScheduled).toList(),
|
||||
events.where((element) => !element.hasTimestamp()).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -104,12 +104,12 @@ class UnscheduleEventsBloc
|
||||
|
||||
void _startListening() {
|
||||
final onDatabaseChanged = DatabaseCallbacks(
|
||||
onRowsCreated: (rowIds) async {
|
||||
onRowsCreated: (rows) async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
for (final id in rowIds) {
|
||||
final event = await _loadEvent(id);
|
||||
for (final row in rows) {
|
||||
final event = await _loadEvent(row.rowMeta.id);
|
||||
if (event != null && !isClosed) {
|
||||
add(UnscheduleEventsEvent.didReceiveEvent(event));
|
||||
}
|
||||
|
||||
+33
-5
@@ -1,3 +1,6 @@
|
||||
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';
|
||||
@@ -11,7 +14,6 @@ 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';
|
||||
@@ -31,6 +33,8 @@ import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
|
||||
class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
final _toggleExtension = ToggleExtensionNotifier();
|
||||
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
@@ -52,6 +56,7 @@ class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
return CalendarSettingBar(
|
||||
key: _makeValueKey(controller),
|
||||
databaseController: controller,
|
||||
toggleExtension: _toggleExtension,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +65,18 @@ class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
) {
|
||||
return SizedBox.fromSize();
|
||||
return DatabaseViewSettingExtension(
|
||||
key: _makeValueKey(controller),
|
||||
viewId: controller.viewId,
|
||||
databaseController: controller,
|
||||
toggleExtension: _toggleExtension,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_toggleExtension.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
@@ -152,6 +168,18 @@ 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) {
|
||||
@@ -351,10 +379,10 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
void showEventDetails({
|
||||
required BuildContext context,
|
||||
required DatabaseController databaseController,
|
||||
required CalendarEventPB event,
|
||||
required RowMetaPB rowMeta,
|
||||
}) {
|
||||
final rowController = RowController(
|
||||
rowMeta: event.rowMeta,
|
||||
rowMeta: rowMeta,
|
||||
viewId: databaseController.viewId,
|
||||
rowCache: databaseController.rowCache,
|
||||
);
|
||||
@@ -505,7 +533,7 @@ class UnscheduleEventsList extends StatelessWidget {
|
||||
} else {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: event,
|
||||
rowMeta: event.rowMeta,
|
||||
databaseController: databaseController,
|
||||
);
|
||||
PopoverContainer.of(context).close();
|
||||
|
||||
+1
-1
@@ -335,7 +335,7 @@ Widget _toggleItem({
|
||||
const Spacer(),
|
||||
Toggle(
|
||||
value: value,
|
||||
onChanged: (value) => onToggle(!value),
|
||||
onChanged: (value) => onToggle(value),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
|
||||
+34
-9
@@ -1,26 +1,51 @@
|
||||
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 SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SettingButton(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.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-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
@@ -18,12 +17,16 @@ class ChecklistCellBackendService {
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> create({
|
||||
required String name,
|
||||
int? index,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..insertOptions.add(name);
|
||||
final insert = ChecklistCellInsertPB()..name = name;
|
||||
if (index != null) {
|
||||
insert.index = index;
|
||||
}
|
||||
|
||||
final payload = ChecklistCellDataChangesetPB()
|
||||
..cellId = _makdeCellId()
|
||||
..insertTask.add(insert);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
@@ -31,11 +34,9 @@ class ChecklistCellBackendService {
|
||||
Future<FlowyResult<void, FlowyError>> delete({
|
||||
required List<String> optionIds,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..deleteOptionIds.addAll(optionIds);
|
||||
final payload = ChecklistCellDataChangesetPB()
|
||||
..cellId = _makdeCellId()
|
||||
..deleteTasks.addAll(optionIds);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
@@ -43,11 +44,9 @@ class ChecklistCellBackendService {
|
||||
Future<FlowyResult<void, FlowyError>> select({
|
||||
required String optionId,
|
||||
}) {
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..selectedOptionIds.add(optionId);
|
||||
final payload = ChecklistCellDataChangesetPB()
|
||||
..cellId = _makdeCellId()
|
||||
..completedTasks.add(optionId);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
@@ -60,12 +59,28 @@ class ChecklistCellBackendService {
|
||||
final newOption = option.rebuild((option) {
|
||||
option.name = name;
|
||||
});
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..updateOptions.add(newOption);
|
||||
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()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -117,7 +117,10 @@ class FilterEditorBloc extends Bloc<FilterEditorEvent, FilterEditorState> {
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final now = DateTime.now();
|
||||
final timestamp =
|
||||
DateTime(now.year, now.month, now.day).millisecondsSinceEpoch ~/
|
||||
1000;
|
||||
return _filterBackendSvc.insertDateFilter(
|
||||
filterId: filterId,
|
||||
fieldId: fieldId,
|
||||
|
||||
-45
@@ -6,12 +6,6 @@ abstract class SelectOptionFilterDelegate {
|
||||
const SelectOptionFilterDelegate();
|
||||
|
||||
List<SelectOptionPB> getOptions(FieldInfo fieldInfo);
|
||||
|
||||
Set<String> selectOption(
|
||||
List<String> currentOptionIds,
|
||||
String optionId,
|
||||
SelectOptionFilterConditionPB condition,
|
||||
);
|
||||
}
|
||||
|
||||
class SingleSelectOptionFilterDelegateImpl
|
||||
@@ -23,37 +17,6 @@ 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
|
||||
@@ -66,12 +29,4 @@ 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,6 +46,14 @@ 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(
|
||||
@@ -75,9 +83,6 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
|
||||
databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);
|
||||
},
|
||||
didReceiveGridUpdate: (grid) {
|
||||
emit(state.copyWith(grid: grid));
|
||||
},
|
||||
didReceiveFieldUpdate: (fields) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -112,20 +117,22 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
);
|
||||
}
|
||||
|
||||
RowCache getRowCache(RowId rowId) => databaseController.rowCache;
|
||||
RowCache get rowCache => 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) {
|
||||
@@ -176,6 +183,7 @@ 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;
|
||||
@@ -188,10 +196,6 @@ 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) =
|
||||
@@ -202,7 +206,6 @@ 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,
|
||||
@@ -220,7 +223,6 @@ class GridState with _$GridState {
|
||||
rowInfos: [],
|
||||
rowCount: 0,
|
||||
createdRow: null,
|
||||
grid: null,
|
||||
viewId: viewId,
|
||||
reorderable: true,
|
||||
loadingState: const LoadingState.loading(),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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';
|
||||
@@ -18,6 +19,7 @@ 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';
|
||||
@@ -143,18 +145,7 @@ class _GridPageState extends State<GridPage> {
|
||||
}
|
||||
},
|
||||
child: BlocConsumer<GridBloc, GridState>(
|
||||
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;
|
||||
},
|
||||
),
|
||||
listener: listener,
|
||||
builder: (context, state) => state.loadingState.map(
|
||||
loading: (_) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
@@ -185,7 +176,7 @@ class _GridPageState extends State<GridPage> {
|
||||
) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final gridBloc = context.read<GridBloc>();
|
||||
final rowCache = gridBloc.getRowCache(rowId);
|
||||
final rowCache = gridBloc.rowCache;
|
||||
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
|
||||
if (rowMeta == null) {
|
||||
return;
|
||||
@@ -210,6 +201,48 @@ 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 {
|
||||
@@ -292,21 +325,41 @@ 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 = widget.scrollController.verticalController
|
||||
.position.maxScrollExtent >
|
||||
0;
|
||||
showFloatingCalculations =
|
||||
verticalController.position.maxScrollExtent > 0;
|
||||
|
||||
isAtBottom = verticalController.position.atEdge &&
|
||||
verticalController.offset > 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -356,11 +409,11 @@ class _GridRowsState extends State<_GridRows> {
|
||||
) {
|
||||
// 1. GridRowBottomBar
|
||||
// 2. GridCalculationsRow
|
||||
// 3. Footer Padding
|
||||
final itemCount = state.rowInfos.length + 3;
|
||||
return Stack(
|
||||
final itemCount =
|
||||
state.rowInfos.length + (showFloatingCalculations ? 1 : 2);
|
||||
return Column(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
Expanded(
|
||||
child: ReorderableListView.builder(
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
@@ -408,38 +461,31 @@ 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) {
|
||||
if (showFloatingCalculations) {
|
||||
return const SizedBox(
|
||||
key: Key('calculations_bottom_padding'),
|
||||
height: 36,
|
||||
);
|
||||
} else {
|
||||
return GridCalculationsRow(
|
||||
key: const Key('grid_calculations'),
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
}
|
||||
if (index == state.rowInfos.length + 1 &&
|
||||
!showFloatingCalculations) {
|
||||
return GridCalculationsRow(
|
||||
key: const Key('grid_calculations'),
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox(key: Key('footer_padding'), height: 10);
|
||||
return _renderRow(
|
||||
context,
|
||||
state.rowInfos[index].rowId,
|
||||
index: index,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (showFloatingCalculations) ...[
|
||||
_PositionedCalculationsRow(viewId: widget.viewId),
|
||||
_PositionedCalculationsRow(
|
||||
viewId: widget.viewId,
|
||||
isAtBottom: isAtBottom,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
@@ -462,7 +508,7 @@ class _GridRowsState extends State<_GridRows> {
|
||||
}
|
||||
|
||||
final child = GridRow(
|
||||
key: ValueKey(rowId),
|
||||
key: ValueKey("grid_row_$rowId"),
|
||||
fieldController: databaseController.fieldController,
|
||||
rowId: rowId,
|
||||
viewId: viewId,
|
||||
@@ -542,10 +588,16 @@ 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();
|
||||
@@ -555,32 +607,28 @@ class _PositionedCalculationsRowState
|
||||
extends State<_PositionedCalculationsRow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+4
-2
@@ -25,12 +25,14 @@ 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: SingleSelectOptionFilterCondition()
|
||||
.conditions
|
||||
actions: conditions
|
||||
.map(
|
||||
(action) => ConditionWrapper(
|
||||
action.$1,
|
||||
|
||||
+4
-14
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -17,14 +16,12 @@ 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;
|
||||
|
||||
@@ -53,20 +50,13 @@ class SelectOptionFilterList extends StatelessWidget {
|
||||
SelectOptionPB option,
|
||||
bool isSelected,
|
||||
) {
|
||||
final selectedOptionIds = Set<String>.from(filter.optionIds);
|
||||
if (isSelected) {
|
||||
final selectedOptionIds = Set<String>.from(filter.optionIds)
|
||||
..remove(option.id);
|
||||
|
||||
_updateSelectOptions(context, filter, selectedOptionIds);
|
||||
selectedOptionIds.remove(option.id);
|
||||
} else {
|
||||
final selectedOptionIds = delegate.selectOption(
|
||||
filter.optionIds,
|
||||
option.id,
|
||||
filter.condition,
|
||||
);
|
||||
|
||||
_updateSelectOptions(context, filter, selectedOptionIds);
|
||||
selectedOptionIds.add(option.id);
|
||||
}
|
||||
_updateSelectOptions(context, filter, selectedOptionIds);
|
||||
onTap();
|
||||
}
|
||||
|
||||
|
||||
+2
-8
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -76,11 +75,7 @@ class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
|
||||
SliverToBoxAdapter(child: _buildFilterPanel(filter, field)),
|
||||
];
|
||||
|
||||
if (![
|
||||
SelectOptionFilterConditionPB.OptionIsEmpty,
|
||||
SelectOptionFilterConditionPB.OptionIsNotEmpty,
|
||||
].contains(filter.condition)) {
|
||||
final delegate = filter.makeDelegate(field);
|
||||
if (filter.canAttachContent) {
|
||||
slivers
|
||||
..add(const SliverToBoxAdapter(child: VSpace(4)))
|
||||
..add(
|
||||
@@ -88,8 +83,7 @@ class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
|
||||
child: SelectOptionFilterList(
|
||||
filter: filter,
|
||||
field: field,
|
||||
delegate: delegate,
|
||||
options: delegate.getOptions(field),
|
||||
options: filter.makeDelegate(field).getOptions(field),
|
||||
onTap: () => popoverMutex.close(),
|
||||
),
|
||||
),
|
||||
|
||||
+2
-1
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -7,7 +9,6 @@ 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 {
|
||||
|
||||
+3
-1
@@ -44,7 +44,9 @@ class MobileTabBarHeader extends StatelessWidget {
|
||||
controller: state
|
||||
.tabBarControllerByViewId[currentView.viewId]!.controller,
|
||||
features: switch (currentView.layout) {
|
||||
ViewLayoutPB.Board => [MobileDatabaseControlFeatures.filter],
|
||||
ViewLayoutPB.Board || ViewLayoutPB.Calendar => [
|
||||
MobileDatabaseControlFeatures.filter,
|
||||
],
|
||||
ViewLayoutPB.Grid => [
|
||||
MobileDatabaseControlFeatures.sort,
|
||||
MobileDatabaseControlFeatures.filter,
|
||||
|
||||
+14
-11
@@ -17,7 +17,6 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
ChecklistCellBloc bloc,
|
||||
ChecklistCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
@@ -27,7 +26,7 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
skipTraversal: true,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
popupBuilder: (popoverContext) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cellContainerNotifier.isFocus = true;
|
||||
});
|
||||
@@ -39,15 +38,19 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {
|
||||
);
|
||||
},
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: state.tasks.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: ChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+309
-99
@@ -1,14 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../editable_cell_skeleton/checklist.dart';
|
||||
@@ -19,150 +26,359 @@ class DesktopRowDetailChecklistCellSkin extends IEditableChecklistCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
ChecklistCellBloc bloc,
|
||||
ChecklistCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return ChecklistItems(
|
||||
return ChecklistRowDetailCell(
|
||||
context: context,
|
||||
cellContainerNotifier: cellContainerNotifier,
|
||||
bloc: bloc,
|
||||
state: state,
|
||||
popoverController: popoverController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistItems extends StatefulWidget {
|
||||
const ChecklistItems({
|
||||
class ChecklistRowDetailCell extends StatefulWidget {
|
||||
const ChecklistRowDetailCell({
|
||||
super.key,
|
||||
required this.context,
|
||||
required this.cellContainerNotifier,
|
||||
required this.bloc,
|
||||
required this.state,
|
||||
required this.popoverController,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
final CellContainerNotifier cellContainerNotifier;
|
||||
final ChecklistCellBloc bloc;
|
||||
final ChecklistCellState state;
|
||||
final PopoverController popoverController;
|
||||
|
||||
@override
|
||||
State<ChecklistItems> createState() => _ChecklistItemsState();
|
||||
State<ChecklistRowDetailCell> createState() => _ChecklistRowDetailCellState();
|
||||
}
|
||||
|
||||
class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
bool showIncompleteOnly = false;
|
||||
|
||||
class _ChecklistRowDetailCellState extends State<ChecklistRowDetailCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tasks = [...widget.state.tasks];
|
||||
if (showIncompleteOnly) {
|
||||
tasks.removeWhere((task) => task.isSelected);
|
||||
}
|
||||
// final children = tasks
|
||||
// .mapIndexed(
|
||||
// (index, task) => Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
// child: ChecklistItem(
|
||||
// key: ValueKey('${task.data.id}$index'),
|
||||
// task: task,
|
||||
// autofocus: widget.state.newTask && index == tasks.length - 1,
|
||||
// onSubmitted: () {
|
||||
// if (index == tasks.length - 1) {
|
||||
// // create a new task under the last task if the users press enter
|
||||
// widget.bloc.add(const ChecklistCellEvent.createNewTask(''));
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ChecklistProgressBar(
|
||||
tasks: widget.state.tasks,
|
||||
percent: widget.state.percent,
|
||||
),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
FlowyIconButton(
|
||||
tooltipText: showIncompleteOnly
|
||||
? LocaleKeys.grid_checklist_showComplete.tr()
|
||||
: LocaleKeys.grid_checklist_hideComplete.tr(),
|
||||
width: 32,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
icon: FlowySvg(
|
||||
showIncompleteOnly ? FlowySvgs.show_m : FlowySvgs.hide_m,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(
|
||||
() => showIncompleteOnly = !showIncompleteOnly,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
ProgressAndHideCompleteButton(
|
||||
onToggleHideComplete: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.toggleShowIncompleteOnly()),
|
||||
),
|
||||
const VSpace(2.0),
|
||||
_ChecklistCellEditors(tasks: tasks),
|
||||
ChecklistItemControl(cellNotifer: widget.cellContainerNotifier),
|
||||
_ChecklistItems(
|
||||
onStartCreatingTaskAfter: (index) {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(ChecklistCellEvent.updatePhantomIndex(index + 1));
|
||||
},
|
||||
),
|
||||
ChecklistItemControl(
|
||||
cellNotifer: widget.cellContainerNotifier,
|
||||
onTap: () {
|
||||
final bloc = context.read<ChecklistCellBloc>();
|
||||
if (bloc.state.phantomIndex == null) {
|
||||
bloc.add(
|
||||
ChecklistCellEvent.updatePhantomIndex(
|
||||
bloc.state.showIncompleteOnly
|
||||
? bloc.state.tasks
|
||||
.where((task) => !task.isSelected)
|
||||
.length
|
||||
: bloc.state.tasks.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistCellEditors extends StatelessWidget {
|
||||
const _ChecklistCellEditors({
|
||||
required this.tasks,
|
||||
@visibleForTesting
|
||||
class ProgressAndHideCompleteButton extends StatelessWidget {
|
||||
const ProgressAndHideCompleteButton({
|
||||
super.key,
|
||||
required this.onToggleHideComplete,
|
||||
});
|
||||
|
||||
final List<ChecklistSelectOption> tasks;
|
||||
final VoidCallback onToggleHideComplete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<ChecklistCellBloc>();
|
||||
final state = bloc.state;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...tasks.mapIndexed(
|
||||
(index, task) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ChecklistItem(
|
||||
key: ValueKey('${task.data.id}$index'),
|
||||
task: task,
|
||||
autofocus: state.newTask && index == tasks.length - 1,
|
||||
onSubmitted: () {
|
||||
if (index == tasks.length - 1) {
|
||||
// create a new task under the last task if the users press enter
|
||||
bloc.add(const ChecklistCellEvent.createNewTask(''));
|
||||
}
|
||||
},
|
||||
),
|
||||
return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.showIncompleteOnly != current.showIncompleteOnly,
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
return ChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
FlowyIconButton(
|
||||
tooltipText: state.showIncompleteOnly
|
||||
? LocaleKeys.grid_checklist_showComplete.tr()
|
||||
: LocaleKeys.grid_checklist_hideComplete.tr(),
|
||||
width: 32,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
icon: FlowySvg(
|
||||
state.showIncompleteOnly
|
||||
? FlowySvgs.show_m
|
||||
: FlowySvgs.hide_m,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
onPressed: onToggleHideComplete,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistItems extends StatelessWidget {
|
||||
const _ChecklistItems({
|
||||
required this.onStartCreatingTaskAfter,
|
||||
});
|
||||
|
||||
final void Function(int index) onStartCreatingTaskAfter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Actions(
|
||||
actions: {
|
||||
_CancelCreatingFromPhantomIntent:
|
||||
CallbackAction<_CancelCreatingFromPhantomIntent>(
|
||||
onInvoke: (_CancelCreatingFromPhantomIntent intent) {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.updatePhantomIndex(null));
|
||||
return;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
final children = _makeChildren(context, state);
|
||||
return ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
proxyDecorator: (child, index, _) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
BlocProvider.value(
|
||||
value: context.read<ChecklistCellBloc>(),
|
||||
child: child,
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: children.length,
|
||||
itemBuilder: (_, index) => children[index],
|
||||
onReorder: (from, to) {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(ChecklistCellEvent.reorderTask(from, to));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _makeChildren(BuildContext context, ChecklistCellState state) {
|
||||
final children = <Widget>[];
|
||||
|
||||
final tasks = [...state.tasks];
|
||||
|
||||
if (state.showIncompleteOnly) {
|
||||
tasks.removeWhere((task) => task.isSelected);
|
||||
}
|
||||
|
||||
children.addAll(
|
||||
tasks.mapIndexed(
|
||||
(index, task) => Padding(
|
||||
key: ValueKey('checklist_row_detail_cell_task_${task.data.id}'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ChecklistItem(
|
||||
task: task,
|
||||
index: index,
|
||||
onSubmitted: () {
|
||||
onStartCreatingTaskAfter(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (state.phantomIndex != null) {
|
||||
children.insert(
|
||||
state.phantomIndex!,
|
||||
Padding(
|
||||
key: const ValueKey('new_checklist_cell_task'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: PhantomChecklistItem(index: state.phantomIndex!),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
class _CancelCreatingFromPhantomIntent extends Intent {
|
||||
const _CancelCreatingFromPhantomIntent();
|
||||
}
|
||||
|
||||
class _SubmitPhantomTaskIntent extends Intent {
|
||||
const _SubmitPhantomTaskIntent({
|
||||
required this.taskDescription,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final String taskDescription;
|
||||
final int index;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class PhantomChecklistItem extends StatefulWidget {
|
||||
const PhantomChecklistItem({
|
||||
super.key,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final int index;
|
||||
|
||||
@override
|
||||
State<PhantomChecklistItem> createState() => _PhantomChecklistItemState();
|
||||
}
|
||||
|
||||
class _PhantomChecklistItemState extends State<PhantomChecklistItem> {
|
||||
TextEditingController textController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
bool isComposing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
textController.addListener(() {
|
||||
setState(() {
|
||||
isComposing = !textController.value.composing.isCollapsed;
|
||||
});
|
||||
});
|
||||
focusNode.addListener(() {
|
||||
if (!focusNode.hasFocus) {
|
||||
textController.clear();
|
||||
Actions.maybeInvoke(
|
||||
context,
|
||||
const _CancelCreatingFromPhantomIntent(),
|
||||
);
|
||||
}
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textController.dispose();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Actions(
|
||||
actions: {
|
||||
_SubmitPhantomTaskIntent: CallbackAction<_SubmitPhantomTaskIntent>(
|
||||
onInvoke: (_SubmitPhantomTaskIntent intent) {
|
||||
if (intent.taskDescription.isNotEmpty) {
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.createNewTask(
|
||||
intent.taskDescription,
|
||||
index: intent.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
textController.clear();
|
||||
return;
|
||||
},
|
||||
),
|
||||
},
|
||||
child: Shortcuts(
|
||||
shortcuts: _buildShortcuts(),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 32),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Center(
|
||||
child: ChecklistCellTextfield(
|
||||
textController: textController,
|
||||
focusNode: focusNode,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<ShortcutActivator, Intent> _buildShortcuts() {
|
||||
return isComposing
|
||||
? const {}
|
||||
: {
|
||||
const SingleActivator(LogicalKeyboardKey.enter):
|
||||
_SubmitPhantomTaskIntent(
|
||||
taskDescription: textController.text,
|
||||
index: widget.index,
|
||||
),
|
||||
const SingleActivator(LogicalKeyboardKey.escape):
|
||||
const _CancelCreatingFromPhantomIntent(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class ChecklistItemControl extends StatelessWidget {
|
||||
const ChecklistItemControl({super.key, required this.cellNotifer});
|
||||
const ChecklistItemControl({
|
||||
super.key,
|
||||
required this.cellNotifer,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CellContainerNotifier cellNotifer;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -171,9 +387,7 @@ class ChecklistItemControl extends StatelessWidget {
|
||||
child: Consumer<CellContainerNotifier>(
|
||||
builder: (buildContext, notifier, _) => GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.createNewTask("")),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0),
|
||||
height: 12,
|
||||
@@ -192,11 +406,7 @@ class ChecklistItemControl extends StatelessWidget {
|
||||
maximumSize: const Size.square(12),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(
|
||||
const ChecklistCellEvent.createNewTask(""),
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
|
||||
+5
-11
@@ -29,7 +29,6 @@ abstract class IEditableChecklistCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
ChecklistCellBloc bloc,
|
||||
ChecklistCellState state,
|
||||
PopoverController popoverController,
|
||||
);
|
||||
}
|
||||
@@ -70,16 +69,11 @@ class GridChecklistCellState extends GridCellState<EditableChecklistCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
return widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
state,
|
||||
_popover,
|
||||
);
|
||||
},
|
||||
child: widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
_popover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+30
-27
@@ -17,35 +17,38 @@ class MobileGridChecklistCellSkin extends IEditableChecklistCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
ChecklistCellBloc bloc,
|
||||
ChecklistCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
hoverColor: Colors.transparent,
|
||||
text: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: state.tasks.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: ChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontSize: 15),
|
||||
),
|
||||
),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const MobileChecklistCellEditScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
hoverColor: Colors.transparent,
|
||||
text: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: state.tasks.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: ChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontSize: 15),
|
||||
),
|
||||
),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const MobileChecklistCellEditScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+42
-39
@@ -19,49 +19,52 @@ class MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
ChecklistCellBloc bloc,
|
||||
ChecklistCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
backgroundColor: AFThemeExtension.of(context).background,
|
||||
builder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const MobileChecklistCellEditScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 48,
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: state.tasks.isEmpty
|
||||
? FlowyText(
|
||||
LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).hintColor,
|
||||
)
|
||||
: ChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
backgroundColor: AFThemeExtension.of(context).background,
|
||||
builder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const MobileChecklistCellEditScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 48,
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: state.tasks.isEmpty
|
||||
? FlowyText(
|
||||
LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).hintColor,
|
||||
)
|
||||
: ChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+120
-49
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
@@ -108,23 +109,46 @@ class ChecklistItemList extends StatelessWidget {
|
||||
final itemList = options
|
||||
.mapIndexed(
|
||||
(index, option) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
|
||||
key: ValueKey(option.data.id),
|
||||
child: ChecklistItem(
|
||||
task: option,
|
||||
index: index,
|
||||
onSubmitted: index == options.length - 1 ? onUpdateTask : null,
|
||||
key: ValueKey(option.data.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Flexible(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (context, index) => itemList[index],
|
||||
separatorBuilder: (context, index) => const VSpace(4),
|
||||
itemCount: itemList.length,
|
||||
child: ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
proxyDecorator: (child, index, _) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
BlocProvider.value(
|
||||
value: context.read<ChecklistCellBloc>(),
|
||||
child: child,
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) => itemList[index],
|
||||
itemCount: itemList.length,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
onReorder: (from, to) {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(ChecklistCellEvent.reorderTask(from, to));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -142,17 +166,17 @@ class _UpdateTaskDescriptionIntent extends Intent {
|
||||
const _UpdateTaskDescriptionIntent();
|
||||
}
|
||||
|
||||
/// Represents an existing task
|
||||
@visibleForTesting
|
||||
class ChecklistItem extends StatefulWidget {
|
||||
const ChecklistItem({
|
||||
super.key,
|
||||
required this.task,
|
||||
required this.index,
|
||||
this.onSubmitted,
|
||||
this.autofocus = false,
|
||||
});
|
||||
|
||||
final ChecklistSelectOption task;
|
||||
final int index;
|
||||
final VoidCallback? onSubmitted;
|
||||
final bool autofocus;
|
||||
|
||||
@@ -167,27 +191,21 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
|
||||
bool isHovered = false;
|
||||
bool isFocused = false;
|
||||
bool isComposing = false;
|
||||
|
||||
final _debounceOnChanged = Debounce(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
final selectTaskShortcut = {
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey.enter,
|
||||
meta: Platform.isMacOS,
|
||||
control: !Platform.isMacOS,
|
||||
): const _SelectTaskIntent(),
|
||||
const SingleActivator(LogicalKeyboardKey.enter):
|
||||
const _UpdateTaskDescriptionIntent(),
|
||||
const SingleActivator(LogicalKeyboardKey.escape):
|
||||
const _EndEditingTaskIntent(),
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
textController.text = widget.task.data.name;
|
||||
textController.addListener(() {
|
||||
setState(() {
|
||||
isComposing = !textController.value.composing.isCollapsed;
|
||||
});
|
||||
});
|
||||
if (widget.autofocus) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
@@ -196,6 +214,15 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
if (!focusNode.hasFocus &&
|
||||
oldWidget.task.data.name != widget.task.data.name) {
|
||||
textController.text = widget.task.data.name;
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceOnChanged.dispose();
|
||||
@@ -208,9 +235,8 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFocusedOrHovered =
|
||||
isHovered || isFocused || textFieldFocusNode.hasFocus;
|
||||
final color = isFocusedOrHovered
|
||||
final isFocusedOrHovered = isHovered || isFocused;
|
||||
final color = isFocusedOrHovered || textFieldFocusNode.hasFocus
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: Colors.transparent;
|
||||
return FocusableActionDetector(
|
||||
@@ -222,7 +248,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
isFocused = value;
|
||||
}),
|
||||
actions: _buildActions(),
|
||||
shortcuts: selectTaskShortcut,
|
||||
shortcuts: _buildShortcuts(),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: GridSize.popoverItemHeight,
|
||||
@@ -232,25 +258,46 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: _buildChild(
|
||||
context,
|
||||
isFocusedOrHovered,
|
||||
isFocusedOrHovered && !textFieldFocusNode.hasFocus,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChild(BuildContext context, bool isFocusedOrHovered) {
|
||||
Widget _buildChild(bool showTrash) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ChecklistCellCheckIcon(task: widget.task),
|
||||
ReorderableDragStartListener(
|
||||
index: widget.index,
|
||||
child: MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grab,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 32,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.drag_element_s,
|
||||
size: const Size.square(14),
|
||||
color: AFThemeExtension.of(context).onBackground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ChecklistCellCheckIcon(
|
||||
task: widget.task,
|
||||
),
|
||||
Expanded(
|
||||
child: ChecklistCellTextfield(
|
||||
textController: textController,
|
||||
focusNode: textFieldFocusNode,
|
||||
autofocus: widget.autofocus,
|
||||
onChanged: () {
|
||||
_debounceOnChanged.call(() {
|
||||
if (textController.selection.isCollapsed) {
|
||||
if (!isComposing) {
|
||||
_submitUpdateTaskDescription(textController.text);
|
||||
}
|
||||
});
|
||||
@@ -266,7 +313,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (isFocusedOrHovered)
|
||||
if (showTrash)
|
||||
ChecklistCellDeleteButton(
|
||||
onPressed: () => context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.deleteTask(widget.task.data.id),
|
||||
@@ -276,6 +323,22 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
);
|
||||
}
|
||||
|
||||
Map<ShortcutActivator, Intent> _buildShortcuts() {
|
||||
return {
|
||||
SingleActivator(
|
||||
LogicalKeyboardKey.enter,
|
||||
meta: Platform.isMacOS,
|
||||
control: !Platform.isMacOS,
|
||||
): const _SelectTaskIntent(),
|
||||
if (!isComposing)
|
||||
const SingleActivator(LogicalKeyboardKey.enter):
|
||||
const _UpdateTaskDescriptionIntent(),
|
||||
if (!isComposing)
|
||||
const SingleActivator(LogicalKeyboardKey.escape):
|
||||
const _EndEditingTaskIntent(),
|
||||
};
|
||||
}
|
||||
|
||||
Map<Type, Action<Intent>> _buildActions() {
|
||||
return {
|
||||
_SelectTaskIntent: CallbackAction<_SelectTaskIntent>(
|
||||
@@ -326,12 +389,19 @@ class NewTaskItem extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NewTaskItemState extends State<NewTaskItem> {
|
||||
final _textEditingController = TextEditingController();
|
||||
bool _isCreateButtonEnabled = false;
|
||||
final textController = TextEditingController();
|
||||
|
||||
bool isCreateButtonEnabled = false;
|
||||
bool isComposing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
textController.addListener(() {
|
||||
setState(() {
|
||||
isComposing = !textController.value.composing.isCollapsed;
|
||||
});
|
||||
});
|
||||
if (widget.focusNode.canRequestFocus) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
@@ -339,7 +409,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -353,13 +423,15 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.enter): () =>
|
||||
_createNewTask(context),
|
||||
},
|
||||
bindings: isComposing
|
||||
? const {}
|
||||
: {
|
||||
const SingleActivator(LogicalKeyboardKey.enter): () =>
|
||||
_createNewTask(context),
|
||||
},
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
controller: _textEditingController,
|
||||
controller: textController,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
@@ -373,8 +445,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
),
|
||||
onSubmitted: (_) => _createNewTask(context),
|
||||
onChanged: (value) => setState(
|
||||
() => _isCreateButtonEnabled =
|
||||
_textEditingController.text.isNotEmpty,
|
||||
() => isCreateButtonEnabled = textController.text.isNotEmpty,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -382,23 +453,23 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
FlowyTextButton(
|
||||
LocaleKeys.grid_checklist_submitNewTask.tr(),
|
||||
fontSize: 11,
|
||||
fillColor: _isCreateButtonEnabled
|
||||
fillColor: isCreateButtonEnabled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).disabledColor,
|
||||
hoverColor: _isCreateButtonEnabled
|
||||
hoverColor: isCreateButtonEnabled
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).disabledColor,
|
||||
fontColor: Theme.of(context).colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
onPressed: _isCreateButtonEnabled
|
||||
onPressed: isCreateButtonEnabled
|
||||
? () {
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.createNewTask(
|
||||
_textEditingController.text,
|
||||
textController.text,
|
||||
),
|
||||
);
|
||||
widget.focusNode.requestFocus();
|
||||
_textEditingController.clear();
|
||||
textController.clear();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@@ -408,12 +479,12 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
}
|
||||
|
||||
void _createNewTask(BuildContext context) {
|
||||
final taskDescription = _textEditingController.text;
|
||||
final taskDescription = textController.text;
|
||||
if (taskDescription.isNotEmpty) {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(ChecklistCellEvent.createNewTask(taskDescription));
|
||||
_textEditingController.clear();
|
||||
textController.clear();
|
||||
}
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
|
||||
+9
-9
@@ -40,23 +40,22 @@ class ChecklistCellTextfield extends StatelessWidget {
|
||||
super.key,
|
||||
required this.textController,
|
||||
required this.focusNode,
|
||||
required this.autofocus,
|
||||
required this.onChanged,
|
||||
this.onChanged,
|
||||
this.contentPadding = const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 2,
|
||||
),
|
||||
this.onSubmitted,
|
||||
});
|
||||
|
||||
final TextEditingController textController;
|
||||
final FocusNode focusNode;
|
||||
final bool autofocus;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
final VoidCallback? onSubmitted;
|
||||
final VoidCallback onChanged;
|
||||
final VoidCallback? onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const contentPadding = EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 2.0,
|
||||
);
|
||||
return TextField(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
@@ -65,11 +64,12 @@ class ChecklistCellTextfield extends StatelessWidget {
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
isDense: true,
|
||||
contentPadding: contentPadding,
|
||||
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
||||
),
|
||||
textInputAction: onSubmitted == null ? TextInputAction.next : null,
|
||||
onChanged: (_) => onChanged(),
|
||||
onChanged: (_) => onChanged?.call(),
|
||||
onSubmitted: (_) => onSubmitted?.call(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,12 +52,12 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
includeTime: state.includeTime,
|
||||
rebuildOnDaySelected: false,
|
||||
onIncludeTimeChanged: (value) =>
|
||||
dateCellBloc.add(DateCellEditorEvent.setIncludeTime(!value)),
|
||||
dateCellBloc.add(DateCellEditorEvent.setIncludeTime(value)),
|
||||
isRange: state.isRange,
|
||||
startDay: state.isRange ? state.startDay : null,
|
||||
endDay: state.isRange ? state.endDay : null,
|
||||
onIsRangeChanged: (value) =>
|
||||
dateCellBloc.add(DateCellEditorEvent.setIsRange(!value)),
|
||||
dateCellBloc.add(DateCellEditorEvent.setIsRange(value)),
|
||||
dateFormat: state.dateTypeOptionPB.dateFormat,
|
||||
timeFormat: state.dateTypeOptionPB.timeFormat,
|
||||
selectedDay: state.dateTime,
|
||||
|
||||
+83
-47
@@ -27,21 +27,17 @@ class _MobileChecklistCellEditScreenState
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(height: 420),
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const DragHandle(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _buildHeader(context),
|
||||
),
|
||||
const Divider(),
|
||||
const Expanded(child: _TaskList()),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const DragHandle(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _buildHeader(context),
|
||||
),
|
||||
const Divider(),
|
||||
const Expanded(child: _TaskList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -75,19 +71,42 @@ class _TaskList extends StatelessWidget {
|
||||
state.tasks
|
||||
.mapIndexed(
|
||||
(index, task) => _ChecklistItem(
|
||||
key: ValueKey('mobile_checklist_task_${task.data.id}'),
|
||||
task: task,
|
||||
autofocus: state.newTask && index == state.tasks.length - 1,
|
||||
index: index,
|
||||
autofocus: state.phantomIndex != null &&
|
||||
index == state.tasks.length - 1,
|
||||
onAutofocus: () {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.updatePhantomIndex(null));
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
cells.add(const _NewTaskButton());
|
||||
cells.add(
|
||||
const _NewTaskButton(key: ValueKey('mobile_checklist_new_task')),
|
||||
);
|
||||
|
||||
return ListView.separated(
|
||||
return ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
proxyDecorator: (child, index, _) => Material(
|
||||
color: Colors.transparent,
|
||||
child: BlocProvider.value(
|
||||
value: context.read<ChecklistCellBloc>(),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (_, __) => const VSpace(8),
|
||||
itemBuilder: (_, int index) => cells[index],
|
||||
itemBuilder: (_, index) => cells[index],
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
onReorder: (from, to) {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(ChecklistCellEvent.reorderTask(from, to));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -95,26 +114,37 @@ class _TaskList extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ChecklistItem extends StatefulWidget {
|
||||
const _ChecklistItem({required this.task, required this.autofocus});
|
||||
const _ChecklistItem({
|
||||
super.key,
|
||||
required this.task,
|
||||
required this.index,
|
||||
required this.autofocus,
|
||||
this.onAutofocus,
|
||||
});
|
||||
|
||||
final ChecklistSelectOption task;
|
||||
final int index;
|
||||
final bool autofocus;
|
||||
final VoidCallback? onAutofocus;
|
||||
|
||||
@override
|
||||
State<_ChecklistItem> createState() => _ChecklistItemState();
|
||||
}
|
||||
|
||||
class _ChecklistItemState extends State<_ChecklistItem> {
|
||||
late final TextEditingController _textController;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
late final TextEditingController textController;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
Timer? _debounceOnChanged;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController(text: widget.task.data.name);
|
||||
textController = TextEditingController(text: widget.task.data.name);
|
||||
if (widget.autofocus) {
|
||||
_focusNode.requestFocus();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
widget.onAutofocus?.call();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,47 +152,50 @@ class _ChecklistItemState extends State<_ChecklistItem> {
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.task.data.name != oldWidget.task.data.name &&
|
||||
!_focusNode.hasFocus) {
|
||||
_textController.text = widget.task.data.name;
|
||||
!focusNode.hasFocus) {
|
||||
textController.text = widget.task.data.name;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_focusNode.dispose();
|
||||
textController.dispose();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
|
||||
constraints: const BoxConstraints(minHeight: 44),
|
||||
child: Row(
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(ChecklistCellEvent.selectTask(widget.task.data.id)),
|
||||
child: SizedBox.square(
|
||||
dimension: 44,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
widget.task.isSelected
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
size: const Size.square(20.0),
|
||||
blendMode: BlendMode.dst,
|
||||
ReorderableDelayedDragStartListener(
|
||||
index: widget.index,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(ChecklistCellEvent.selectTask(widget.task.data.id)),
|
||||
child: SizedBox.square(
|
||||
dimension: 44,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
widget.task.isSelected
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
size: const Size.square(20.0),
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
focusNode: _focusNode,
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
@@ -261,7 +294,7 @@ class _ChecklistItemState extends State<_ChecklistItem> {
|
||||
}
|
||||
|
||||
class _NewTaskButton extends StatelessWidget {
|
||||
const _NewTaskButton();
|
||||
const _NewTaskButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -270,6 +303,9 @@ class _NewTaskButton extends StatelessWidget {
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.updatePhantomIndex(-1));
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.createNewTask(""));
|
||||
|
||||
@@ -629,23 +629,23 @@ class FieldNameTextField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
FocusNode focusNode = FocusNode();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
focusNode.addListener(() {
|
||||
if (focusNode.hasFocus) {
|
||||
widget.popoverMutex?.close();
|
||||
}
|
||||
});
|
||||
focusNode.addListener(_onFocusChanged);
|
||||
widget.popoverMutex?.addPopoverListener(_onPopoverChanged);
|
||||
}
|
||||
|
||||
widget.popoverMutex?.listenOnPopoverChanged(() {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
});
|
||||
@override
|
||||
void dispose() {
|
||||
widget.popoverMutex?.removePopoverListener(_onPopoverChanged);
|
||||
focusNode.removeListener(_onFocusChanged);
|
||||
focusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -662,15 +662,16 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.removeListener(() {
|
||||
if (focusNode.hasFocus) {
|
||||
widget.popoverMutex?.close();
|
||||
}
|
||||
});
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
void _onFocusChanged() {
|
||||
if (focusNode.hasFocus) {
|
||||
widget.popoverMutex?.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _onPopoverChanged() {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ class MediaTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
value: typeOption.hideFileNames,
|
||||
onChanged: (value) {
|
||||
onTypeOptionUpdated(
|
||||
_toggleHideFiles(typeOption, !value).writeToBuffer(),
|
||||
_toggleHideFiles(typeOption, value).writeToBuffer(),
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
|
||||
+24
-22
@@ -206,22 +206,23 @@ class CreateOptionTextField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||
late final FocusNode _focusNode;
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode()
|
||||
..addListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
widget.popoverMutex?.close();
|
||||
}
|
||||
});
|
||||
widget.popoverMutex?.listenOnPopoverChanged(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
});
|
||||
|
||||
focusNode.addListener(_onFocusChanged);
|
||||
widget.popoverMutex?.addPopoverListener(_onPopoverChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.popoverMutex?.removePopoverListener(_onPopoverChanged);
|
||||
focusNode.removeListener(_onFocusChanged);
|
||||
focusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -234,7 +235,7 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||
child: FlowyTextField(
|
||||
autoClearWhenDone: true,
|
||||
text: text,
|
||||
focusNode: _focusNode,
|
||||
focusNode: focusNode,
|
||||
onCanceled: () {
|
||||
context
|
||||
.read<SelectOptionTypeOptionBloc>()
|
||||
@@ -252,15 +253,16 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
widget.popoverMutex?.close();
|
||||
}
|
||||
});
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
void _onFocusChanged() {
|
||||
if (focusNode.hasFocus) {
|
||||
widget.popoverMutex?.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _onPopoverChanged() {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class TimestampTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
onChanged: (value) {
|
||||
final newTypeOption = _updateTypeOption(
|
||||
typeOption: typeOption,
|
||||
includeTime: !value,
|
||||
includeTime: value,
|
||||
);
|
||||
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
||||
},
|
||||
|
||||
@@ -72,7 +72,7 @@ class DatabaseGroupList extends StatelessWidget {
|
||||
Toggle(
|
||||
value: !state.layoutSettings.hideUngroupedColumn,
|
||||
onChanged: (value) =>
|
||||
_updateLayoutSettings(state.layoutSettings, value),
|
||||
_updateLayoutSettings(state.layoutSettings, !value),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/row/row_document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
@@ -12,7 +12,6 @@ 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:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RowDocument extends StatelessWidget {
|
||||
const RowDocument({
|
||||
@@ -45,7 +44,7 @@ class RowDocument extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
finish: () => _RowEditor(
|
||||
viewPB: state.viewPB!,
|
||||
view: state.viewPB!,
|
||||
onIsEmptyChanged: (isEmpty) => context
|
||||
.read<RowDocumentBloc>()
|
||||
.add(RowDocumentEvent.updateIsEmpty(isEmpty)),
|
||||
@@ -59,84 +58,72 @@ class RowDocument extends StatelessWidget {
|
||||
|
||||
class _RowEditor extends StatelessWidget {
|
||||
const _RowEditor({
|
||||
required this.viewPB,
|
||||
required this.view,
|
||||
this.onIsEmptyChanged,
|
||||
});
|
||||
|
||||
final ViewPB viewPB;
|
||||
final ViewPB view;
|
||||
final void Function(bool)? onIsEmptyChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
// Due to how DropTarget works, there is no way to differentiate if an overlay is
|
||||
// blocking the target visibly, so when we have an overlay with a drop target,
|
||||
// we should disable the drop target for the Editor, until it is closed.
|
||||
//
|
||||
// See FileBlockComponent for sample use.
|
||||
//
|
||||
// Relates to:
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/2
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/331
|
||||
//
|
||||
create: (_) => EditorDropManagerState(),
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentBloc(documentId: viewPB.id)
|
||||
..add(const DocumentEvent.initial()),
|
||||
child: BlocConsumer<DocumentBloc, DocumentState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isDocumentEmpty != current.isDocumentEmpty,
|
||||
listener: (_, state) {
|
||||
if (state.isDocumentEmpty != null) {
|
||||
onIsEmptyChanged?.call(state.isDocumentEmpty!);
|
||||
}
|
||||
if (state.error != null) {
|
||||
Log.error('RowEditor error: ${state.error}');
|
||||
}
|
||||
if (state.editorState == null) {
|
||||
Log.error('RowEditor unable to get editorState');
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
return BlocProvider(
|
||||
create: (_) =>
|
||||
DocumentBloc(documentId: view.id)..add(const DocumentEvent.initial()),
|
||||
child: BlocConsumer<DocumentBloc, DocumentState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isDocumentEmpty != current.isDocumentEmpty,
|
||||
listener: (_, state) {
|
||||
if (state.isDocumentEmpty != null) {
|
||||
onIsEmptyChanged?.call(state.isDocumentEmpty!);
|
||||
}
|
||||
if (state.error != null) {
|
||||
Log.error('RowEditor error: ${state.error}');
|
||||
}
|
||||
if (state.editorState == null) {
|
||||
Log.error('RowEditor unable to get editorState');
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final editorState = state.editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
return Center(
|
||||
child: AppFlowyErrorPage(error: error),
|
||||
);
|
||||
}
|
||||
final editorState = state.editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
return Center(
|
||||
child: AppFlowyErrorPage(error: error),
|
||||
);
|
||||
}
|
||||
|
||||
return Consumer<EditorDropManagerState>(
|
||||
builder: (_, dropState, __) => BlocProvider<ViewInfoBloc>(
|
||||
create: (context) => ViewInfoBloc(view: viewPB),
|
||||
child: IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 300),
|
||||
child: AppFlowyEditorPage(
|
||||
shrinkWrap: true,
|
||||
autoFocus: false,
|
||||
editorState: editorState,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: const EdgeInsets.only(left: 16, right: 54),
|
||||
),
|
||||
showParagraphPlaceholder: (editorState, node) =>
|
||||
editorState.document.isEmpty,
|
||||
placeholderText: (node) =>
|
||||
LocaleKeys.cardDetails_notesPlaceholder.tr(),
|
||||
return BlocProvider<ViewInfoBloc>(
|
||||
create: (context) => ViewInfoBloc(view: view),
|
||||
child: IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 300),
|
||||
child: EditorDropHandler(
|
||||
viewId: view.id,
|
||||
editorState: editorState,
|
||||
isLocalMode: context.read<DocumentBloc>().isLocalMode,
|
||||
child: AppFlowyEditorPage(
|
||||
shrinkWrap: true,
|
||||
autoFocus: false,
|
||||
editorState: editorState,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: const EdgeInsets.only(left: 16, right: 54),
|
||||
),
|
||||
showParagraphPlaceholder: (editorState, _) =>
|
||||
editorState.document.isEmpty,
|
||||
placeholderText: (_) =>
|
||||
LocaleKeys.cardDetails_notesPlaceholder.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+14
-7
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart';
|
||||
@@ -105,15 +106,20 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
|
||||
}
|
||||
|
||||
Widget _buildEditorPage(BuildContext context, DocumentState state) {
|
||||
final appflowyEditorPage = AppFlowyEditorPage(
|
||||
final appflowyEditorPage = EditorDropHandler(
|
||||
viewId: widget.view.id,
|
||||
editorState: state.editorState!,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
isLocalMode: context.read<DocumentBloc>().isLocalMode,
|
||||
child: AppFlowyEditorPage(
|
||||
editorState: state.editorState!,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
),
|
||||
header: _buildDatabaseDataContent(context, state.editorState!),
|
||||
initialSelection: widget.initialSelection,
|
||||
useViewInfoBloc: false,
|
||||
),
|
||||
header: _buildDatabaseDataContent(context, state.editorState!),
|
||||
initialSelection: widget.initialSelection,
|
||||
useViewInfoBloc: false,
|
||||
);
|
||||
|
||||
return Column(
|
||||
@@ -182,6 +188,7 @@ class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
|
||||
|
||||
Widget _buildBanner(BuildContext context) {
|
||||
return DocumentBanner(
|
||||
viewName: widget.view.name,
|
||||
onRestore: () => context.read<DocumentBloc>().add(
|
||||
const DocumentEvent.restorePage(),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -36,6 +38,41 @@ class DocumentService {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<FlowyResult<(DocumentDataPB, BlockPB, Node), FlowyError>>
|
||||
getDocumentNode({
|
||||
required String documentId,
|
||||
required String blockId,
|
||||
}) async {
|
||||
final documentResult = await getDocument(documentId: documentId);
|
||||
final document = documentResult.fold((l) => l, (f) => null);
|
||||
if (document == null) {
|
||||
Log.error('unable to get the document for page $documentId');
|
||||
return FlowyResult.failure(FlowyError(msg: 'Document not found'));
|
||||
}
|
||||
|
||||
final blockResult = await getBlockFromDocument(
|
||||
document: document,
|
||||
blockId: blockId,
|
||||
);
|
||||
final block = blockResult.fold((l) => l, (f) => null);
|
||||
if (block == null) {
|
||||
Log.error(
|
||||
'unable to get the block $blockId from the document $documentId',
|
||||
);
|
||||
return FlowyResult.failure(FlowyError(msg: 'Block not found'));
|
||||
}
|
||||
|
||||
final node = document.buildNode(blockId);
|
||||
if (node == null) {
|
||||
Log.error(
|
||||
'unable to get the node for block $blockId in document $documentId',
|
||||
);
|
||||
return FlowyResult.failure(FlowyError(msg: 'Node not found'));
|
||||
}
|
||||
|
||||
return FlowyResult.success((document, block, node));
|
||||
}
|
||||
|
||||
Future<FlowyResult<BlockPB, FlowyError>> getBlockFromDocument({
|
||||
required DocumentDataPB document,
|
||||
required String blockId,
|
||||
|
||||
+6
-4
@@ -281,8 +281,8 @@ extension on UpdateOperation {
|
||||
assert(parentId.isNotEmpty);
|
||||
|
||||
// create the external text if the node contains the delta in its data.
|
||||
final prevDelta = oldAttributes['delta'];
|
||||
final delta = attributes['delta'];
|
||||
final prevDelta = oldAttributes[blockComponentDelta];
|
||||
final delta = attributes[blockComponentDelta];
|
||||
final diff = prevDelta != null && delta != null
|
||||
? Delta.fromJson(prevDelta).diff(
|
||||
Delta.fromJson(delta),
|
||||
@@ -290,6 +290,7 @@ extension on UpdateOperation {
|
||||
: null;
|
||||
|
||||
final composedAttributes = composeAttributes(oldAttributes, attributes);
|
||||
final composedDelta = composedAttributes?[blockComponentDelta];
|
||||
composedAttributes?.remove(blockComponentDelta);
|
||||
|
||||
final payload = BlockActionPayloadPB()
|
||||
@@ -306,12 +307,13 @@ extension on UpdateOperation {
|
||||
if (textId == null || textId.isEmpty) {
|
||||
// to be compatible with the old version, we create a new text id if the text id is empty.
|
||||
final textId = nanoid(6);
|
||||
final textDeltaPayloadPB = delta == null
|
||||
final textDelta = composedDelta ?? delta ?? prevDelta;
|
||||
final textDeltaPayloadPB = textDelta == null
|
||||
? null
|
||||
: TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(delta),
|
||||
delta: jsonEncode(textDelta),
|
||||
);
|
||||
|
||||
node.externalValues = ExternalValues(
|
||||
|
||||
@@ -51,6 +51,7 @@ class DocumentPlugin extends Plugin {
|
||||
required ViewPB view,
|
||||
required PluginType pluginType,
|
||||
this.initialSelection,
|
||||
this.initialBlockId,
|
||||
}) : notifier = ViewPluginNotifier(view: view) {
|
||||
_pluginType = pluginType;
|
||||
}
|
||||
@@ -61,13 +62,18 @@ class DocumentPlugin extends Plugin {
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
|
||||
// the initial selection of the document
|
||||
final Selection? initialSelection;
|
||||
|
||||
// the initial block id of the document
|
||||
final String? initialBlockId;
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder(
|
||||
bloc: _viewInfoBloc,
|
||||
notifier: notifier,
|
||||
initialSelection: initialSelection,
|
||||
initialBlockId: initialBlockId,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -95,6 +101,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
required this.bloc,
|
||||
required this.notifier,
|
||||
this.initialSelection,
|
||||
this.initialBlockId,
|
||||
});
|
||||
|
||||
final ViewInfoBloc bloc;
|
||||
@@ -102,6 +109,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
ViewPB get view => notifier.view;
|
||||
int? deletedViewIndex;
|
||||
final Selection? initialSelection;
|
||||
final String? initialBlockId;
|
||||
|
||||
@override
|
||||
EdgeInsets get contentPadding => EdgeInsets.zero;
|
||||
@@ -129,6 +137,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
view: view,
|
||||
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
|
||||
initialSelection: initialSelection,
|
||||
initialBlockId: initialBlockId,
|
||||
fixedTitle: fixedTitle,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,55 +1,42 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.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/multi_image_block_component/multi_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy/shared/patterns/file_type_patterns.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.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_editor/appflowy_editor.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
const _excludeFromDropTarget = [
|
||||
ImageBlockKeys.type,
|
||||
CustomImageBlockKeys.type,
|
||||
MultiImageBlockKeys.type,
|
||||
FileBlockKeys.type,
|
||||
];
|
||||
|
||||
class DocumentPage extends StatefulWidget {
|
||||
const DocumentPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
required this.onDeleted,
|
||||
this.initialSelection,
|
||||
this.initialBlockId,
|
||||
this.fixedTitle,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final VoidCallback onDeleted;
|
||||
final Selection? initialSelection;
|
||||
final String? initialBlockId;
|
||||
final String? fixedTitle;
|
||||
|
||||
@override
|
||||
@@ -62,25 +49,16 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
late final documentBloc = DocumentBloc(documentId: widget.view.id)
|
||||
..add(const DocumentEvent.initial());
|
||||
|
||||
StreamSubscription<(TransactionTime, Transaction)>? transactionSubscription;
|
||||
|
||||
bool isUndoRedo = false;
|
||||
bool isPaste = false;
|
||||
bool isDraggingNode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
EditorNotification.addListener(onEditorNotification);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
EditorNotification.removeListener(onEditorNotification);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
documentBloc.close();
|
||||
transactionSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -96,55 +74,37 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
// Due to how DropTarget works, there is no way to differentiate if an overlay is
|
||||
// blocking the target visibly, so when we have an overlay with a drop target,
|
||||
// we should disable the drop target for the Editor, until it is closed.
|
||||
//
|
||||
// See FileBlockComponent for sample use.
|
||||
//
|
||||
// Relates to:
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/2
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/331
|
||||
//
|
||||
create: (_) => EditorDropManagerState(),
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
],
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
buildWhen: shouldRebuildDocument,
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
],
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
buildWhen: shouldRebuildDocument,
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final editorState = state.editorState;
|
||||
this.editorState = editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
Log.error(error);
|
||||
return Center(child: AppFlowyErrorPage(error: error));
|
||||
}
|
||||
final editorState = state.editorState;
|
||||
this.editorState = editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
Log.error(error);
|
||||
return Center(child: AppFlowyErrorPage(error: error));
|
||||
}
|
||||
|
||||
if (state.forceClose) {
|
||||
widget.onDeleted();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (state.forceClose) {
|
||||
widget.onDeleted();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
editorState.transactionStream.listen(onEditorTransaction);
|
||||
|
||||
return BlocListener<ActionNavigationBloc, ActionNavigationState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: onNotificationAction,
|
||||
child: Consumer<EditorDropManagerState>(
|
||||
builder: (context, dropState, _) =>
|
||||
buildEditorPage(context, state, dropState),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
return BlocListener<ActionNavigationBloc, ActionNavigationState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: onNotificationAction,
|
||||
child: buildEditorPage(context, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -152,15 +112,19 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
Widget buildEditorPage(
|
||||
BuildContext context,
|
||||
DocumentState state,
|
||||
EditorDropManagerState dropState,
|
||||
) {
|
||||
final editorState = state.editorState;
|
||||
if (editorState == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final width = context.read<DocumentAppearanceCubit>().state.width;
|
||||
|
||||
final Widget child;
|
||||
if (UniversalPlatform.isMobile) {
|
||||
child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
|
||||
builder: (context, styleState) => AppFlowyEditorPage(
|
||||
editorState: state.editorState!,
|
||||
editorState: editorState,
|
||||
// if the view's name is empty, focus on the title
|
||||
autoFocus: widget.view.name.isEmpty ? false : null,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
@@ -173,85 +137,12 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
),
|
||||
);
|
||||
} else {
|
||||
child = DropTarget(
|
||||
enable: dropState.isDropEnabled,
|
||||
onDragExited: (_) =>
|
||||
state.editorState!.selectionService.removeDropTarget(),
|
||||
onDragUpdated: (details) {
|
||||
final data = state.editorState!.selectionService
|
||||
.getDropTargetRenderData(details.globalPosition);
|
||||
|
||||
if (data != null &&
|
||||
data.dropPath != null &&
|
||||
|
||||
// We implement custom Drop logic for image blocks, this is
|
||||
// how we can exclude them from the Drop Target
|
||||
!_excludeFromDropTarget.contains(data.cursorNode?.type)) {
|
||||
// Render the drop target
|
||||
state.editorState!.selectionService
|
||||
.renderDropTargetForOffset(details.globalPosition);
|
||||
} else {
|
||||
state.editorState!.selectionService.removeDropTarget();
|
||||
}
|
||||
},
|
||||
onDragDone: (details) async {
|
||||
final editorState = state.editorState;
|
||||
if (editorState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorState.selectionService.removeDropTarget();
|
||||
|
||||
final data = editorState.selectionService
|
||||
.getDropTargetRenderData(details.globalPosition);
|
||||
|
||||
if (data != null) {
|
||||
final cursorNode = data.cursorNode;
|
||||
final dropPath = data.dropPath;
|
||||
|
||||
if (cursorNode != null && dropPath != null) {
|
||||
if (_excludeFromDropTarget.contains(cursorNode.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(dropPath);
|
||||
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
|
||||
final List<XFile> imageFiles = [];
|
||||
final List<XFile> otherFiles = [];
|
||||
|
||||
for (final file in details.files) {
|
||||
final fileName = file.name.toLowerCase();
|
||||
if (file.mimeType?.startsWith('image/') ??
|
||||
false || imgExtensionRegex.hasMatch(fileName)) {
|
||||
imageFiles.add(file);
|
||||
} else {
|
||||
otherFiles.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
await editorState.dropImages(
|
||||
node,
|
||||
imageFiles,
|
||||
widget.view.id,
|
||||
isLocalMode,
|
||||
);
|
||||
|
||||
await editorState.dropFiles(
|
||||
node,
|
||||
otherFiles,
|
||||
widget.view.id,
|
||||
isLocalMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child = EditorDropHandler(
|
||||
viewId: widget.view.id,
|
||||
editorState: editorState,
|
||||
isLocalMode: context.read<DocumentBloc>().isLocalMode,
|
||||
child: AppFlowyEditorPage(
|
||||
editorState: state.editorState!,
|
||||
editorState: editorState,
|
||||
// if the view's name is empty, focus on the title
|
||||
autoFocus: widget.view.name.isEmpty ? false : null,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
@@ -260,24 +151,29 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
padding: EditorStyleCustomizer.documentPadding,
|
||||
),
|
||||
header: buildCoverAndIcon(context, state),
|
||||
initialSelection: widget.initialSelection,
|
||||
initialSelection: _calculateInitialSelection(editorState),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Provider(
|
||||
create: (_) => SharedEditorContext(),
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.isDeleted) buildBanner(context),
|
||||
Expanded(child: child),
|
||||
],
|
||||
child: EditorTransactionService(
|
||||
viewId: widget.view.id,
|
||||
editorState: state.editorState!,
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.isDeleted) buildBanner(context),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBanner(BuildContext context) {
|
||||
return DocumentBanner(
|
||||
viewName: widget.view.nameOrDefault,
|
||||
onRestore: () =>
|
||||
context.read<DocumentBloc>().add(const DocumentEvent.restorePage()),
|
||||
onDelete: () => context
|
||||
@@ -313,47 +209,58 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
);
|
||||
}
|
||||
|
||||
void onEditorNotification(EditorNotificationType type) {
|
||||
final editorState = this.editorState;
|
||||
if (editorState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([EditorNotificationType.undo, EditorNotificationType.redo]
|
||||
.contains(type)) {
|
||||
isUndoRedo = true;
|
||||
} else if (type == EditorNotificationType.paste) {
|
||||
isPaste = true;
|
||||
} else if (type == EditorNotificationType.dragStart) {
|
||||
isDraggingNode = true;
|
||||
} else if (type == EditorNotificationType.dragEnd) {
|
||||
isDraggingNode = false;
|
||||
}
|
||||
|
||||
if (type == EditorNotificationType.undo) {
|
||||
undoCommand.execute(editorState);
|
||||
} else if (type == EditorNotificationType.redo) {
|
||||
redoCommand.execute(editorState);
|
||||
} else if (type == EditorNotificationType.exitEditing &&
|
||||
editorState.selection != null) {
|
||||
editorState.selection = null;
|
||||
}
|
||||
}
|
||||
|
||||
void onNotificationAction(
|
||||
BuildContext context,
|
||||
ActionNavigationState state,
|
||||
) {
|
||||
if (state.action != null && state.action!.type == ActionType.jumpToBlock) {
|
||||
final path = state.action?.arguments?[ActionArgumentKeys.nodePath];
|
||||
final action = state.action;
|
||||
if (action == null ||
|
||||
action.type != ActionType.jumpToBlock ||
|
||||
action.objectId != widget.view.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
final editorState = context.read<DocumentBloc>().state.editorState;
|
||||
if (editorState != null && widget.view.id == state.action?.objectId) {
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [path])),
|
||||
);
|
||||
final editorState = context.read<DocumentBloc>().state.editorState;
|
||||
if (editorState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Path? path = _getPathFromAction(action, editorState);
|
||||
if (path != null) {
|
||||
debugPrint('jump to block: $path');
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: path)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Path? _getPathFromAction(NavigationAction action, EditorState editorState) {
|
||||
Path? path = action.arguments?[ActionArgumentKeys.nodePath];
|
||||
if (path == null || path.isEmpty) {
|
||||
final blockId = action.arguments?[ActionArgumentKeys.blockId];
|
||||
if (blockId != null) {
|
||||
path = _findNodePathByBlockId(editorState, blockId);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Path? _findNodePathByBlockId(EditorState editorState, String blockId) {
|
||||
final document = editorState.document;
|
||||
final startNode = document.root.children.firstOrNull;
|
||||
if (startNode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final nodeIterator = NodeIterator(document: document, startNode: startNode);
|
||||
while (nodeIterator.moveNext()) {
|
||||
final node = nodeIterator.current;
|
||||
if (node.id == blockId) {
|
||||
return node.path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool shouldRebuildDocument(DocumentState previous, DocumentState current) {
|
||||
@@ -383,85 +290,23 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Node> collectMatchingNodes(Node node, String type) {
|
||||
final List<Node> matchingNodes = [];
|
||||
if (node.type == type) {
|
||||
matchingNodes.add(node);
|
||||
Selection? _calculateInitialSelection(EditorState editorState) {
|
||||
if (widget.initialSelection != null) {
|
||||
return widget.initialSelection;
|
||||
}
|
||||
|
||||
for (final child in node.children) {
|
||||
matchingNodes.addAll(collectMatchingNodes(child, type));
|
||||
}
|
||||
|
||||
return matchingNodes;
|
||||
}
|
||||
|
||||
void onEditorTransaction((TransactionTime, Transaction) event) {
|
||||
if (editorState == null || event.$1 == TransactionTime.before) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, List<Node>> addedNodes = {
|
||||
for (final handler in SharedEditorContext.transactionHandlers)
|
||||
handler.blockType: [],
|
||||
};
|
||||
final Map<String, List<Node>> removedNodes = {
|
||||
for (final handler in SharedEditorContext.transactionHandlers)
|
||||
handler.blockType: [],
|
||||
};
|
||||
|
||||
final transactionHandlerTypes = SharedEditorContext.transactionHandlers
|
||||
.map((h) => h.blockType)
|
||||
.toList();
|
||||
|
||||
// Collect all matching nodes in a performant way for each handler type.
|
||||
for (final op in event.$2.operations) {
|
||||
if (op is InsertOperation) {
|
||||
for (final n in op.nodes) {
|
||||
for (final handlerType in transactionHandlerTypes) {
|
||||
if (n.type == handlerType) {
|
||||
addedNodes[handlerType]!
|
||||
.addAll(collectMatchingNodes(n, handlerType));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (op is DeleteOperation) {
|
||||
for (final n in op.nodes) {
|
||||
for (final handlerType in transactionHandlerTypes) {
|
||||
if (n.type == handlerType) {
|
||||
removedNodes[handlerType]!
|
||||
.addAll(collectMatchingNodes(n, handlerType));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (widget.initialBlockId != null) {
|
||||
final path = _findNodePathByBlockId(editorState, widget.initialBlockId!);
|
||||
if (path != null) {
|
||||
editorState.selectionType = SelectionType.block;
|
||||
return Selection.collapsed(
|
||||
Position(
|
||||
path: path,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (removedNodes.isEmpty && addedNodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final handler in SharedEditorContext.transactionHandlers) {
|
||||
final added = addedNodes[handler.blockType] ?? [];
|
||||
final removed = removedNodes[handler.blockType] ?? [];
|
||||
|
||||
if (added.isEmpty && removed.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
handler.onTransaction(
|
||||
context,
|
||||
editorState!,
|
||||
added,
|
||||
removed,
|
||||
isUndoRedo: isUndoRedo,
|
||||
isPaste: isPaste,
|
||||
isDraggingNode: isDraggingNode,
|
||||
parentViewId: widget.view.id,
|
||||
);
|
||||
|
||||
isUndoRedo = false;
|
||||
isPaste = false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/base_styled_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
||||
class DocumentBanner extends StatelessWidget {
|
||||
const DocumentBanner({
|
||||
super.key,
|
||||
required this.viewName,
|
||||
required this.onRestore,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
final String viewName;
|
||||
final void Function() onRestore;
|
||||
final void Function() onDelete;
|
||||
|
||||
@@ -58,7 +61,16 @@ class DocumentBanner extends StatelessWidget {
|
||||
highlightColor: Theme.of(context).colorScheme.error,
|
||||
outlineColor: colorScheme.tertiaryContainer,
|
||||
borderRadius: Corners.s8Border,
|
||||
onPressed: onDelete,
|
||||
onPressed: () => showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: viewName.trim().isEmpty
|
||||
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
|
||||
: viewName,
|
||||
description: LocaleKeys
|
||||
.deletePagePrompt_deletePermanentDescription
|
||||
.tr(),
|
||||
onConfirm: onDelete,
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
||||
color: colorScheme.tertiary,
|
||||
|
||||
+554
-265
@@ -18,7 +18,17 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
/// Build the block component builders.
|
||||
///
|
||||
/// Every block type should have a corresponding builder in the map.
|
||||
/// Otherwise, the errorBlockComponentBuilder will be rendered.
|
||||
///
|
||||
/// Additional, you can define the block render options in the builder
|
||||
/// - customize the block option actions. (... button and + button)
|
||||
/// - customize the block component configuration. (padding, placeholder, etc.)
|
||||
/// - customize the block icon. (bulleted list, numbered list, todo list)
|
||||
/// - customize the hover menu. (show the menu at the top-right corner of the block)
|
||||
Map<String, BlockComponentBuilder> buildBlockComponentBuilders({
|
||||
required BuildContext context,
|
||||
required EditorState editorState,
|
||||
required EditorStyleCustomizer styleCustomizer,
|
||||
@@ -28,18 +38,32 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
String Function(Node)? placeholderText,
|
||||
EdgeInsets? customHeadingPadding,
|
||||
}) {
|
||||
final standardActions = [
|
||||
OptionAction.delete,
|
||||
OptionAction.duplicate,
|
||||
// Copy link to block feature is disable temporarily, enable this test when the feature is ready.
|
||||
// filter out the copy link to block option if in local mode
|
||||
// if (context.read<DocumentBloc?>()?.isLocalMode != true)
|
||||
// OptionAction.copyLinkToBlock,
|
||||
];
|
||||
final configuration = _buildDefaultConfiguration(context);
|
||||
final builders = _buildBlockComponentBuilderMap(
|
||||
context,
|
||||
configuration: configuration,
|
||||
editorState: editorState,
|
||||
styleCustomizer: styleCustomizer,
|
||||
showParagraphPlaceholder: showParagraphPlaceholder,
|
||||
placeholderText: placeholderText,
|
||||
);
|
||||
|
||||
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
|
||||
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
||||
if (editable) {
|
||||
_customBlockOptionActions(
|
||||
context,
|
||||
builders: builders,
|
||||
editorState: editorState,
|
||||
styleCustomizer: styleCustomizer,
|
||||
slashMenuItems: slashMenuItems,
|
||||
);
|
||||
}
|
||||
|
||||
return builders;
|
||||
}
|
||||
|
||||
BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) {
|
||||
final configuration = BlockComponentConfiguration(
|
||||
// use EdgeInsets.zero to remove the default padding.
|
||||
padding: (_) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
final pageStyle = context.read<DocumentPageStyleBloc>().state;
|
||||
@@ -54,218 +78,206 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
? const EdgeInsets.only(left: 26.0)
|
||||
: const EdgeInsets.only(right: 26.0),
|
||||
);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/// Build the option actions for the block component.
|
||||
///
|
||||
/// Notes: different block type may have different option actions.
|
||||
/// All the block types have the delete and duplicate options.
|
||||
List<OptionAction> _buildOptionActions(BuildContext context, String type) {
|
||||
final standardActions = [
|
||||
OptionAction.delete,
|
||||
OptionAction.duplicate,
|
||||
];
|
||||
|
||||
// filter out the copy link to block option if in local mode
|
||||
if (context.read<DocumentBloc?>()?.isLocalMode != true) {
|
||||
standardActions.add(OptionAction.copyLinkToBlock);
|
||||
}
|
||||
|
||||
if (EditorOptionActionType.turnInto.supportTypes.contains(type)) {
|
||||
standardActions.add(OptionAction.turnInto);
|
||||
}
|
||||
|
||||
if (EditorOptionActionType.color.supportTypes.contains(type)) {
|
||||
standardActions.addAll([OptionAction.divider, OptionAction.color]);
|
||||
}
|
||||
|
||||
if (EditorOptionActionType.align.supportTypes.contains(type)) {
|
||||
standardActions.addAll([OptionAction.divider, OptionAction.align]);
|
||||
}
|
||||
|
||||
if (EditorOptionActionType.depth.supportTypes.contains(type)) {
|
||||
standardActions.addAll([OptionAction.divider, OptionAction.depth]);
|
||||
}
|
||||
|
||||
return standardActions;
|
||||
}
|
||||
|
||||
void _customBlockOptionActions(
|
||||
BuildContext context, {
|
||||
required Map<String, BlockComponentBuilder> builders,
|
||||
required EditorState editorState,
|
||||
required EditorStyleCustomizer styleCustomizer,
|
||||
List<SelectionMenuItem>? slashMenuItems,
|
||||
}) {
|
||||
for (final entry in builders.entries) {
|
||||
if (entry.key == PageBlockKeys.type) {
|
||||
continue;
|
||||
}
|
||||
final builder = entry.value;
|
||||
final actions = _buildOptionActions(context, entry.key);
|
||||
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
builder.showActions =
|
||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||
builder.configuration = builder.configuration.copyWith(
|
||||
blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric(
|
||||
vertical: 1,
|
||||
),
|
||||
);
|
||||
|
||||
builder.actionBuilder = (context, state) {
|
||||
final top = builder.configuration.padding(context.node).top;
|
||||
final padding = context.node.type == HeadingBlockKeys.type
|
||||
? EdgeInsets.only(top: top + 8.0)
|
||||
: EdgeInsets.only(top: top + 2.0);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: BlockActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: editorState,
|
||||
blockComponentBuilder: builders,
|
||||
actions: actions,
|
||||
showSlashMenu: slashMenuItems != null
|
||||
? () => customSlashCommand(
|
||||
slashMenuItems,
|
||||
shouldInsertSlash: false,
|
||||
style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||
).handler.call(editorState)
|
||||
: () {},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, BlockComponentBuilder> _buildBlockComponentBuilderMap(
|
||||
BuildContext context, {
|
||||
required BlockComponentConfiguration configuration,
|
||||
required EditorState editorState,
|
||||
required EditorStyleCustomizer styleCustomizer,
|
||||
ShowPlaceholder? showParagraphPlaceholder,
|
||||
String Function(Node)? placeholderText,
|
||||
EdgeInsets? customHeadingPadding,
|
||||
}) {
|
||||
final customBlockComponentBuilderMap = {
|
||||
PageBlockKeys.type: PageBlockComponentBuilder(),
|
||||
ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(placeholderText: placeholderText),
|
||||
showPlaceholder: showParagraphPlaceholder,
|
||||
ParagraphBlockKeys.type: _buildParagraphBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
showParagraphPlaceholder,
|
||||
placeholderText,
|
||||
),
|
||||
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(),
|
||||
),
|
||||
iconBuilder: (_, node, onCheck) =>
|
||||
TodoListIcon(node: node, onCheck: onCheck),
|
||||
toggleChildrenTriggers: [
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.shiftLeft,
|
||||
LogicalKeyboardKey.shiftRight,
|
||||
],
|
||||
TodoListBlockKeys.type: _buildTodoListBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(),
|
||||
),
|
||||
iconBuilder: (_, node) => BulletedListIcon(node: node),
|
||||
BulletedListBlockKeys.type: _buildBulletedListBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(),
|
||||
),
|
||||
iconBuilder: (_, node, textDirection) =>
|
||||
NumberedListIcon(node: node, textDirection: textDirection),
|
||||
NumberedListBlockKeys.type: _buildNumberedListBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(),
|
||||
),
|
||||
QuoteBlockKeys.type: _buildQuoteBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (node) {
|
||||
if (customHeadingPadding != null) {
|
||||
return customHeadingPadding;
|
||||
}
|
||||
|
||||
if (UniversalPlatform.isMobile) {
|
||||
final pageStyle = context.read<DocumentPageStyleBloc>().state;
|
||||
final factor = pageStyle.fontLayout.factor;
|
||||
final headingPaddings = pageStyle.lineHeightLayout.headingPaddings
|
||||
.map((e) => e * factor);
|
||||
int level = node.attributes[HeadingBlockKeys.level] ?? 6;
|
||||
level = level.clamp(1, 6);
|
||||
return EdgeInsets.only(top: headingPaddings.elementAt(level - 1));
|
||||
}
|
||||
|
||||
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
|
||||
},
|
||||
placeholderText: (node) {
|
||||
int level = node.attributes[HeadingBlockKeys.level] ?? 6;
|
||||
level = level.clamp(1, 6);
|
||||
return LocaleKeys.blockPlaceholders_heading.tr(
|
||||
args: [level.toString()],
|
||||
);
|
||||
},
|
||||
),
|
||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||
HeadingBlockKeys.type: _buildHeadingBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
styleCustomizer,
|
||||
customHeadingPadding,
|
||||
),
|
||||
ImageBlockKeys.type: CustomImageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
showMenu: true,
|
||||
menuBuilder: (node, state) => Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: ImageMenu(node: node, state: state),
|
||||
),
|
||||
ImageBlockKeys.type: _buildCustomImageBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
MultiImageBlockKeys.type: MultiImageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
showMenu: true,
|
||||
menuBuilder: (
|
||||
Node node,
|
||||
MultiImageBlockComponentState state,
|
||||
ValueNotifier<int> indexNotifier,
|
||||
VoidCallback onImageDeleted,
|
||||
) =>
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: MultiImageMenu(
|
||||
node: node,
|
||||
state: state,
|
||||
indexNotifier: indexNotifier,
|
||||
isLocalMode: context.read<DocumentBloc>().isLocalMode,
|
||||
onImageDeleted: onImageDeleted,
|
||||
),
|
||||
),
|
||||
MultiImageBlockKeys.type: _buildMultiImageBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
TableBlockKeys.type: TableBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
TableBlockKeys.type: _buildTableBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
TableCellBlockKeys.type: TableCellBlockComponentBuilder(
|
||||
colorBuilder: (context, node) {
|
||||
final String colorString =
|
||||
node.attributes[TableCellBlockKeys.colBackgroundColor] ??
|
||||
node.attributes[TableCellBlockKeys.rowBackgroundColor] ??
|
||||
'';
|
||||
if (colorString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return buildEditorCustomizedColor(context, node, colorString);
|
||||
},
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
TableCellBlockKeys.type: _buildTableCellBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
DatabaseBlockKeys.gridType: _buildDatabaseViewBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
DatabaseBlockKeys.boardType: _buildDatabaseViewBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
DatabaseBlockKeys.calendarType: _buildDatabaseViewBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (node) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
defaultColor: calloutBGColor,
|
||||
CalloutBlockKeys.type: _buildCalloutBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
height: 28.0,
|
||||
wrapper: (_, node, child) => MobileBlockActionButtons(
|
||||
showThreeDots: false,
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
child: child,
|
||||
),
|
||||
DividerBlockKeys.type: _buildDividerBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
editorState,
|
||||
),
|
||||
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
MathEquationBlockKeys.type: _buildMathEquationBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
),
|
||||
styleBuilder: () => CodeBlockStyle(
|
||||
backgroundColor: AFThemeExtension.of(context).calloutBGColor,
|
||||
foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),
|
||||
languagePickerBuilder: codeBlockLanguagePickerBuilder,
|
||||
copyButtonBuilder: codeBlockCopyBuilder,
|
||||
showLineNumbers: false,
|
||||
CodeBlockKeys.type: _buildCodeBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
styleCustomizer,
|
||||
),
|
||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
AutoCompletionBlockKeys.type: _buildAutoCompletionBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
OutlineBlockKeys.type: OutlineBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderTextStyle: (_) =>
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||
),
|
||||
SmartEditBlockKeys.type: _buildSmartEditBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
LinkPreviewBlockKeys.type: LinkPreviewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
cache: LinkPreviewDataCache(),
|
||||
showMenu: true,
|
||||
menuBuilder: (context, node, state) => Positioned(
|
||||
top: 10,
|
||||
right: 0,
|
||||
child: LinkPreviewMenu(node: node, state: state),
|
||||
),
|
||||
builder: (_, node, url, title, description, imageUrl) =>
|
||||
CustomLinkPreviewWidget(
|
||||
node: node,
|
||||
url: url,
|
||||
title: title,
|
||||
description: description,
|
||||
imageUrl: imageUrl,
|
||||
),
|
||||
ToggleListBlockKeys.type: _buildToggleListBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
FileBlockKeys.type: FileBlockComponentBuilder(configuration: configuration),
|
||||
SubPageBlockKeys.type: SubPageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
OutlineBlockKeys.type: _buildOutlineBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
styleCustomizer,
|
||||
),
|
||||
LinkPreviewBlockKeys.type: _buildLinkPreviewBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
FileBlockKeys.type: _buildFileBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
SubPageBlockKeys.type: _buildSubPageBlockComponentBuilder(
|
||||
context,
|
||||
configuration,
|
||||
),
|
||||
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
@@ -277,70 +289,347 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
...customBlockComponentBuilderMap,
|
||||
};
|
||||
|
||||
if (editable) {
|
||||
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
||||
for (final entry in builders.entries) {
|
||||
if (entry.key == PageBlockKeys.type) {
|
||||
continue;
|
||||
}
|
||||
final builder = entry.value;
|
||||
|
||||
// customize the action builder.
|
||||
final supportColorBuilderTypes = [
|
||||
ParagraphBlockKeys.type,
|
||||
HeadingBlockKeys.type,
|
||||
BulletedListBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
OutlineBlockKeys.type,
|
||||
ToggleListBlockKeys.type,
|
||||
];
|
||||
|
||||
final supportAlignBuilderType = [ImageBlockKeys.type];
|
||||
final supportDepthBuilderType = [OutlineBlockKeys.type];
|
||||
final colorAction = [OptionAction.divider, OptionAction.color];
|
||||
final alignAction = [OptionAction.divider, OptionAction.align];
|
||||
final depthAction = [OptionAction.depth];
|
||||
|
||||
final List<OptionAction> actions = [
|
||||
...standardActions,
|
||||
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
||||
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
||||
if (supportDepthBuilderType.contains(entry.key)) ...depthAction,
|
||||
];
|
||||
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
builder.showActions =
|
||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||
|
||||
builder.actionBuilder = (context, state) {
|
||||
final top = builder.configuration.padding(context.node).top;
|
||||
final padding = context.node.type == HeadingBlockKeys.type
|
||||
? EdgeInsets.only(top: top + 8.0)
|
||||
: EdgeInsets.only(top: top + 2.0);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: BlockActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: editorState,
|
||||
blockComponentBuilder: builders,
|
||||
actions: actions,
|
||||
showSlashMenu: slashMenuItems != null
|
||||
? () => customSlashCommand(
|
||||
slashMenuItems,
|
||||
shouldInsertSlash: false,
|
||||
style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||
).handler.call(editorState)
|
||||
: () {},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builders;
|
||||
}
|
||||
|
||||
ParagraphBlockComponentBuilder _buildParagraphBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
ShowPlaceholder? showParagraphPlaceholder,
|
||||
String Function(Node)? placeholderText,
|
||||
) {
|
||||
return ParagraphBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(placeholderText: placeholderText),
|
||||
showPlaceholder: showParagraphPlaceholder,
|
||||
);
|
||||
}
|
||||
|
||||
TodoListBlockComponentBuilder _buildTodoListBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return TodoListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(),
|
||||
),
|
||||
iconBuilder: (_, node, onCheck) => TodoListIcon(
|
||||
node: node,
|
||||
onCheck: onCheck,
|
||||
),
|
||||
toggleChildrenTriggers: [
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.shiftLeft,
|
||||
LogicalKeyboardKey.shiftRight,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
BulletedListBlockComponentBuilder _buildBulletedListBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return BulletedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(),
|
||||
),
|
||||
iconBuilder: (_, node) => BulletedListIcon(node: node),
|
||||
);
|
||||
}
|
||||
|
||||
NumberedListBlockComponentBuilder _buildNumberedListBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return NumberedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(),
|
||||
),
|
||||
iconBuilder: (_, node, textDirection) => NumberedListIcon(
|
||||
node: node,
|
||||
textDirection: textDirection,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return QuoteBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
EditorStyleCustomizer styleCustomizer,
|
||||
EdgeInsets? customHeadingPadding,
|
||||
) {
|
||||
return HeadingBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (node) {
|
||||
if (customHeadingPadding != null) {
|
||||
return customHeadingPadding;
|
||||
}
|
||||
|
||||
if (UniversalPlatform.isMobile) {
|
||||
final pageStyle = context.read<DocumentPageStyleBloc>().state;
|
||||
final factor = pageStyle.fontLayout.factor;
|
||||
final headingPaddings =
|
||||
pageStyle.lineHeightLayout.headingPaddings.map((e) => e * factor);
|
||||
int level = node.attributes[HeadingBlockKeys.level] ?? 6;
|
||||
level = level.clamp(1, 6);
|
||||
return EdgeInsets.only(top: headingPaddings.elementAt(level - 1));
|
||||
}
|
||||
|
||||
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
|
||||
},
|
||||
placeholderText: (node) {
|
||||
int level = node.attributes[HeadingBlockKeys.level] ?? 6;
|
||||
level = level.clamp(1, 6);
|
||||
return LocaleKeys.blockPlaceholders_heading.tr(
|
||||
args: [level.toString()],
|
||||
);
|
||||
},
|
||||
),
|
||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||
);
|
||||
}
|
||||
|
||||
CustomImageBlockComponentBuilder _buildCustomImageBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return CustomImageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
showMenu: true,
|
||||
menuBuilder: (node, state) => Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: ImageMenu(node: node, state: state),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MultiImageBlockComponentBuilder _buildMultiImageBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return MultiImageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
showMenu: true,
|
||||
menuBuilder: (
|
||||
Node node,
|
||||
MultiImageBlockComponentState state,
|
||||
ValueNotifier<int> indexNotifier,
|
||||
VoidCallback onImageDeleted,
|
||||
) =>
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: MultiImageMenu(
|
||||
node: node,
|
||||
state: state,
|
||||
indexNotifier: indexNotifier,
|
||||
isLocalMode: context.read<DocumentBloc>().isLocalMode,
|
||||
onImageDeleted: onImageDeleted,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TableBlockComponentBuilder _buildTableBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return TableBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TableCellBlockComponentBuilder _buildTableCellBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return TableCellBlockComponentBuilder(
|
||||
colorBuilder: (context, node) {
|
||||
final String colorString =
|
||||
node.attributes[TableCellBlockKeys.colBackgroundColor] ??
|
||||
node.attributes[TableCellBlockKeys.rowBackgroundColor] ??
|
||||
'';
|
||||
if (colorString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return buildEditorCustomizedColor(context, node, colorString);
|
||||
},
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DatabaseViewBlockComponentBuilder _buildDatabaseViewBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
|
||||
return CalloutBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (node) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
defaultColor: calloutBGColor,
|
||||
);
|
||||
}
|
||||
|
||||
DividerBlockComponentBuilder _buildDividerBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
EditorState editorState,
|
||||
) {
|
||||
return DividerBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
height: 28.0,
|
||||
wrapper: (_, node, child) => MobileBlockActionButtons(
|
||||
showThreeDots: false,
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MathEquationBlockComponentBuilder _buildMathEquationBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return MathEquationBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
);
|
||||
}
|
||||
|
||||
CodeBlockComponentBuilder _buildCodeBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
EditorStyleCustomizer styleCustomizer,
|
||||
) {
|
||||
return CodeBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
),
|
||||
styleBuilder: () => CodeBlockStyle(
|
||||
backgroundColor: AFThemeExtension.of(context).calloutBGColor,
|
||||
foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),
|
||||
languagePickerBuilder: codeBlockLanguagePickerBuilder,
|
||||
copyButtonBuilder: codeBlockCopyBuilder,
|
||||
showLineNumbers: false,
|
||||
);
|
||||
}
|
||||
|
||||
AutoCompletionBlockComponentBuilder _buildAutoCompletionBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return AutoCompletionBlockComponentBuilder();
|
||||
}
|
||||
|
||||
SmartEditBlockComponentBuilder _buildSmartEditBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return SmartEditBlockComponentBuilder();
|
||||
}
|
||||
|
||||
ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return ToggleListBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
);
|
||||
}
|
||||
|
||||
OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
EditorStyleCustomizer styleCustomizer,
|
||||
) {
|
||||
return OutlineBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderTextStyle: (_) =>
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return LinkPreviewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
cache: LinkPreviewDataCache(),
|
||||
showMenu: true,
|
||||
menuBuilder: (context, node, state) => Positioned(
|
||||
top: 10,
|
||||
right: 0,
|
||||
child: LinkPreviewMenu(node: node, state: state),
|
||||
),
|
||||
builder: (_, node, url, title, description, imageUrl) =>
|
||||
CustomLinkPreviewWidget(
|
||||
node: node,
|
||||
url: url,
|
||||
title: title,
|
||||
description: description,
|
||||
imageUrl: imageUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FileBlockComponentBuilder _buildFileBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return FileBlockComponentBuilder(configuration: configuration);
|
||||
}
|
||||
|
||||
SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder(
|
||||
BuildContext context,
|
||||
BlockComponentConfiguration configuration,
|
||||
) {
|
||||
return SubPageBlockComponentBuilder(configuration: configuration);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block_component.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/multi_image_block_component/multi_image_block_component.dart';
|
||||
import 'package:appflowy/shared/patterns/file_type_patterns.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const _excludeFromDropTarget = [
|
||||
ImageBlockKeys.type,
|
||||
CustomImageBlockKeys.type,
|
||||
MultiImageBlockKeys.type,
|
||||
FileBlockKeys.type,
|
||||
];
|
||||
|
||||
class EditorDropHandler extends StatelessWidget {
|
||||
const EditorDropHandler({
|
||||
super.key,
|
||||
required this.viewId,
|
||||
required this.editorState,
|
||||
required this.isLocalMode,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
final EditorState editorState;
|
||||
final bool isLocalMode;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
// Due to how DropTarget works, there is no way to differentiate if an overlay is
|
||||
// blocking the target visibly, so when we have an overlay with a drop target,
|
||||
// we should disable the drop target for the Editor, until it is closed.
|
||||
//
|
||||
// See FileBlockComponent for sample use.
|
||||
//
|
||||
// Relates to:
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/2
|
||||
// - https://github.com/MixinNetwork/flutter-plugins/issues/331
|
||||
//
|
||||
create: (_) => EditorDropManagerState(),
|
||||
child: Consumer<EditorDropManagerState>(
|
||||
builder: (context, dropState, _) => DropTarget(
|
||||
enable: dropState.isDropEnabled,
|
||||
onDragExited: (_) => editorState.selectionService.removeDropTarget(),
|
||||
onDragUpdated: _onDragUpdated,
|
||||
onDragDone: _onDragDone,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDragUpdated(DropEventDetails details) {
|
||||
final data = editorState.selectionService
|
||||
.getDropTargetRenderData(details.globalPosition);
|
||||
|
||||
if (data != null &&
|
||||
data.dropPath != null &&
|
||||
|
||||
// We implement custom Drop logic for image blocks, this is
|
||||
// how we can exclude them from the Drop Target
|
||||
!_excludeFromDropTarget.contains(data.cursorNode?.type)) {
|
||||
// Render the drop target
|
||||
editorState.selectionService
|
||||
.renderDropTargetForOffset(details.globalPosition);
|
||||
} else {
|
||||
editorState.selectionService.removeDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDragDone(DropDoneDetails details) async {
|
||||
editorState.selectionService.removeDropTarget();
|
||||
|
||||
final data = editorState.selectionService
|
||||
.getDropTargetRenderData(details.globalPosition);
|
||||
|
||||
if (data != null) {
|
||||
final cursorNode = data.cursorNode;
|
||||
final dropPath = data.dropPath;
|
||||
|
||||
if (cursorNode != null && dropPath != null) {
|
||||
if (_excludeFromDropTarget.contains(cursorNode.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(dropPath);
|
||||
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final file in details.files) {
|
||||
final fileName = file.name.toLowerCase();
|
||||
if (file.mimeType?.startsWith('image/') ??
|
||||
false || imgExtensionRegex.hasMatch(fileName)) {
|
||||
await editorState.dropImages(node, [file], viewId, isLocalMode);
|
||||
} else {
|
||||
await editorState.dropFiles(node, [file], viewId, isLocalMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/background
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/child_page.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
@@ -64,6 +66,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
late final InlineActionsService inlineActionsService = InlineActionsService(
|
||||
context: context,
|
||||
handlers: [
|
||||
if (FeatureFlag.inlineSubPageMention.isOn)
|
||||
InlineChildPageService(currentViewId: documentBloc.documentId),
|
||||
InlinePageReferenceService(currentViewId: documentBloc.documentId),
|
||||
DateReferenceService(context),
|
||||
ReminderReferenceService(context),
|
||||
@@ -177,8 +181,14 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
focusManager = AFFocusManager.maybeOf(context);
|
||||
focusManager?.loseFocusNotifier.addListener(_loseFocus);
|
||||
|
||||
if (widget.initialSelection != null) {
|
||||
widget.editorState.updateSelectionWithReason(widget.initialSelection);
|
||||
final initialSelection = widget.initialSelection;
|
||||
final path = initialSelection?.start.path;
|
||||
if (initialSelection != null && path != null && path.isNotEmpty) {
|
||||
editorScrollController.itemScrollController.jumpTo(
|
||||
index: path.first,
|
||||
alignment: 0.5,
|
||||
);
|
||||
widget.editorState.updateSelectionWithReason(initialSelection);
|
||||
}
|
||||
|
||||
widget.editorState.service.keyboardService?.registerInterceptor(
|
||||
@@ -270,10 +280,12 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
context.read<AppearanceSettingsCubit>().state.enableRtlToolbarItems,
|
||||
);
|
||||
|
||||
final isViewDeleted = context.read<DocumentBloc>().state.isDeleted;
|
||||
final editor = Directionality(
|
||||
textDirection: textDirection,
|
||||
child: AppFlowyEditor(
|
||||
editorState: widget.editorState,
|
||||
editable: !isViewDeleted,
|
||||
editorScrollController: editorScrollController,
|
||||
// setup the auto focus parameters
|
||||
autoFocus: widget.autoFocus ?? autoFocus,
|
||||
@@ -281,7 +293,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
// setup the theme
|
||||
editorStyle: styleCustomizer.style(),
|
||||
// customize the block builders
|
||||
blockComponentBuilders: getEditorBuilderMap(
|
||||
blockComponentBuilders: buildBlockComponentBuilders(
|
||||
slashMenuItems: slashMenuItems,
|
||||
context: context,
|
||||
editorState: widget.editorState,
|
||||
@@ -311,6 +323,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
),
|
||||
);
|
||||
|
||||
if (isViewDeleted) {
|
||||
return editor;
|
||||
}
|
||||
|
||||
final editorState = widget.editorState;
|
||||
|
||||
if (UniversalPlatform.isMobile) {
|
||||
|
||||
+3
@@ -12,12 +12,14 @@ class BlockActionButton extends StatelessWidget {
|
||||
required this.richMessage,
|
||||
required this.onTap,
|
||||
this.showTooltip = true,
|
||||
this.onPointerDown,
|
||||
});
|
||||
|
||||
final FlowySvgData svg;
|
||||
final bool showTooltip;
|
||||
final InlineSpan richMessage;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onPointerDown;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -26,6 +28,7 @@ class BlockActionButton extends StatelessWidget {
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grab,
|
||||
child: IgnoreParentGestureWidget(
|
||||
onPress: onPointerDown,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
|
||||
Loaded 100 of 527 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user