Compare commits

..
Author SHA1 Message Date
Zack Fu Zi Xiang f78b5b4561 chore: update dep 2024-11-05 20:30:12 +08:00
Kilu.He f6e002edbd feat: support toggle heading (#6712)
* feat: support toggle heading

* fix: support others markdown
2024-11-05 14:06:38 +08:00
Lucas 82effbf8e4 fix: toggle heading issues (#6714)
* fix: pages overflow when selecting homepage

* fix: toggle heading issues

* Revert "fix: pages overflow when selecting homepage"

This reverts commit 156882a9a7.

* chore: optimize code logic

* fix: assertion in toggle list

* fix: make the turn into menu and color menu exclusive

* test: add toggle heading test
2024-11-05 10:52:22 +08:00
Lucas 54096b391f fix: settings site issues (#6701)
* fix: pages overflow when selecting homepage

* fix: settings site issues

* chore: try to fix windows ci

* test: add tests

* fix: shareblock state update

* fix: pages overflow when selecting homepage

* fix: view name doesn't update after publishing

* chore: add translations

* feat: close popup menu after unpublishing page

* feat: align the published page name with header
2024-11-05 09:23:38 +08:00
Mathias Mogensen 39eee12f53 fix: launch review 0.7.3 (#6718) 2024-11-05 00:39:00 +01:00
Mathias Mogensen bae19b9c3a feat: files media improvements (#6632)
* feat: files & media uiux improvements

* feat: desktop grid media cell changes

* test: amend tests

* feat: media mobile improvements

* feat: more refinement

* fix: dart linter

* chore: update collab

* fix: icon and test

* test: refactor upload hint

* test: forgot to use helper method
2024-11-05 00:13:19 +01:00
Richard Shiue f00e1ebf20 chore: remove focus when starting to create a card from the bottom (#6709) 2024-11-04 14:30:32 +08:00
Kilu.He 939e28d2e1 feat: support tab and shift+tab on range (#6703)
* feat: support tab and shift+tab on range

* feat: add login to no found page
2024-11-04 14:14:16 +08:00
Nathan.fooo 3fbdcab7b0 chore: try to fix when load data from cloud cause windows delay rendering (#6708) 2024-11-04 13:53:02 +08:00
Nathan.fooo a1d0dba0a5 chore: remove local ai watch on windows and linux (#6704)
* chore: remove local ai watch on windows and linux

* chore: clippy
2024-11-04 11:26:05 +08:00
Mathias Mogensen 5e41849f69 fix: remove listeners (#6686) 2024-11-04 10:26:48 +08:00
Richard Shiue cf56e20be9 chore: launch review 0.7.3 (#6698)
* refactor: date picker

* chore: provide guidance to users while using date picker

* fix: row card icon alignment

* fix: untitled database views

* chore: hide hint text while choosing date range

* test: fix widget test

* chore: use current time when toggling include time

* chore: move autofill date logic to date picker

* test: add tests

* chore: also apply to mention date block

* test: fix integration tests

* chore: fix a date picker edge case

* fix: unmatching border radii
2024-11-04 10:11:56 +08:00
Nathan.fooo 1b9b2a5f8d chore: fix database view event (#6697) 2024-11-01 21:48:25 +08:00
Nathan.fooo 690b46a59a chore: fix remove view desc test (#6696)
* chore: fix remove view desc test

* chore: fix remove view desc test

* chore: clippy
2024-11-01 15:57:58 +08:00
Richard Shiue 4d33c87b33 test: fix date picker tests (#6695) 2024-11-01 08:40:43 +01:00
Mathias Mogensen 7cf0c4b16b fix: do not rely on getIt in turnIntoBlock (#6693) 2024-11-01 08:04:02 +01:00
Lucas 910b643db0 chore: bump version 0.7.3 (#6692)
* chore: bump version 0.7.3

* fix: windows ci
2024-11-01 14:23:09 +08:00
Nathan.fooo 0e5ff844b6 chore: remove view desc (#6691)
* chore: remove desc in view

* chore: remove desc in view
2024-11-01 14:18:35 +08:00
Ahad PatelandLucas a9a784f0f0 fix: owner can delete spaces created by other users (#6672)
* fix: owner can delete spaces created by other users

* fix: Update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart

Co-authored-by: Lucas <lucas.xu@appflowy.io>

* fix: add import statement and remove conficting code

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-11-01 12:56:52 +08:00
Mathias Mogensen af390f584e feat: turn into page (sub page block) (#6685)
* feat: turn into page (sub page block)

* fix: bugs and node copyWith

* test: add base test
2024-11-01 05:14:51 +01:00
Lucas eff37d74dc fix: date reminder test (#6681) 2024-11-01 09:33:12 +08:00
Mathias Mogensen 0ea2da424f fix: reminder improvements (#6680) 2024-11-01 00:36:14 +01:00
Lucas 07e34609e7 feat: custom namespace/domain (#6614) 2024-10-31 14:38:32 +08:00
Lucas 74c757d0c8 feat: enable link to database in slash menu (#6673)
* feat: enable link to database in slash menu

* test: add link to page test

* test: remove improper test
2024-10-31 14:12:19 +08:00
Nathan.fooo e6357a9d6c chore: optimize import appflowy data (#6671)
* chore: only import user workspace

* chore: apply migration if need

* chore remove unused code

* chore: store first time installed version

* chore: clippy
2024-10-31 13:02:54 +08:00
Mathias Mogensen 5a047b9d2d fix: add referenced page hint (#6679) 2024-10-31 04:25:26 +01:00
Lucas 873ab6cdc7 feat: scroll to block after selecting notification item (#6667)
* fix: scrollbar's ScrollController has no ScrollPosition attached

* feat: support scrolling to block after selecting notification item

* chore: remove debug print

* fix: unable to cancel block selection
2024-10-31 10:29:34 +08:00
Mathias Mogensen af6736d352 fix: enable sub page block (#6595)
* fix: enable sub page block

* fix: open newly inserted page

* fix: created view should have empty name

* test: use secondary to rename page

* fix: make popover secondary interaction better

* test: amend test

* fix: icon color of sub page block

* test: fix tests

* test: fix hover issue

* feat: clean API for show at cursor on popover
2024-10-30 22:29:01 +01:00
Richard Shiue d5c1955ea3 chore: capitalization for field editor insert left/right (#6676) 2024-10-31 00:12:19 +08:00
Richard Shiue 8bb8131dd4 feat(flutter_desktop): press enter to confirm a dialog (#6675)
* feat: enter to confirm in popup dialog

* test: add test
2024-10-30 23:20:12 +08:00
Richard Shiue 7f4abb5866 chore(flutter_desktop): drop down menu conforms to design (#6674) 2024-10-30 23:19:05 +08:00
Richard Shiue fbe87cc536 fix(flutter_desktop): add forgotten call to update text cell (#6668) 2024-10-30 19:31:10 +08:00
Kilu.He 74165ace0f fix: optimize bundle (#6666)
* feat: open editing

* fix: use cdn to replace build react and react-dom

* fix: replace mention page icon

* fix: replace optimize web bundle
2024-10-30 14:13:52 +08:00
proxycon b576287ac6 chore: update translations with Fink 🐦 (#6656) 2024-10-30 09:29:05 +08:00
Richard Shiue 68d7211735 fix(flutter_desktop): glitchy kanban card editing (#6661)
* test: add test

* fix(flutter_desktop): editing kanban card is glitchy
2024-10-29 22:46:22 +08:00
Richard Shiue dd0dcace87 chore(flutter_desktop): set VisualDensity.standard for entire desktop app (#6648)
* chore: set standard visual density for entire app

* fix: pixel overflow

* fix: pixel overflow

* fix: pixel overflow

* fix: pixel overflow

* fix: text button padding

* chore: slash menu fixes

* chore: slash menu fixes

* ditto
2024-10-29 21:26:04 +08:00
Richard Shiue 8f58c39448 chore(flutter_desktop): improve title and heading appearance (#6658) 2024-10-29 21:25:13 +08:00
Nathan.fooo cee0e2ed42 test: fix ci (#6657) 2024-10-29 20:55:37 +08:00
Lucas 557ce7e006 chore: update appflowy_editor version (#6655)
* fix: foramt link starting at 0 index will lose href attr

* fix: pillcrow icon resize

* fix: integration test

* fix: emoji may not align on Linux
2024-10-29 19:39:56 +08:00
Nathan.fooo d50521d9b6 chore: ignore invalid database when import appflowy data (#6654) 2024-10-29 16:20:05 +08:00
Lucas 28aa2329fb feat: run integration tests on multiple runners (#6637)
* feat: split the integration tests into multiple runners

* feat: use 6 runners

* fix: name

* fix: integration tests

* fix: macos arm64 build

* Revert "fix: macos arm64 build"

This reverts commit 80b961361f.

* chore: use 7 runners

* chore: use 8 runners
2024-10-29 14:55:20 +08:00
Nathan.fooo 9707148b86 chore: bump appflowy collab (#6649)
* chore: fix test

* chore: fix duplicated view name

* chore: fix windows build

* chore: clippy
2024-10-29 11:21:49 +08:00
Lucas 4e739c857b fix: macos arm64 build (#6650) 2024-10-29 10:20:48 +08:00
Nathan.fooo 363fecc7d6 chore: bump client api that includes upload large zip file (#6634)
* chore: bump client api

* chore: update cloud version on CI

* chore: bump client api

* chore: update ci
2024-10-28 21:51:07 +08:00
Kilu.He 6bbaad59a4 fix: redirect to default page (#6638)
* feat: open editing

* fix: add logs

* fix: add logs
2024-10-28 17:55:11 +08:00
Kilu.He 497fcf3bbb fix: support redirect to default page (#6593) 2024-10-28 14:15:59 +08:00
Kilu.He b9ec9f863d fix: import interfaces and error message (#6630) 2024-10-28 11:34:56 +08:00
rensawamo eafd0b3353 fix: crash on delete button press caused by GoRouter (#6629)
* fix: crash on delete button press caused by GoRouter

* fix: gorouter pop
2024-10-28 09:09:11 +08:00
Nathan.fooo a42b6e02ab chore: revamp error for FolderCloudService service (#6623)
* chore: revamp error for FolderCloudService service

* chore: bump cloud

* ci: update configuration

* chore: clippy

* chore: bump client api
2024-10-26 11:20:16 +08:00
Mathias Mogensen 79116e7c7f fix: use drop path rather than node (#6611) 2024-10-25 14:05:19 +02:00
Richard Shiue 128ff594be fix: date picker code improvements and regressions (#6619)
* chore: improve handling result's error variant

* chore: dont call setState in didUpdateWidget

* fix: don't make assumptions on provided data
2024-10-24 20:46:45 +08:00
Lucas c821454df8 Revert "feat: custom domain (#6518)" (#6613)
This reverts commit 73e8b476fb.
2024-10-24 11:10:59 +08:00
ZackandLucas.Xu 73e8b476fb feat: custom domain (#6518)
* feat: api for namespace setting

* feat: add publish timestamp

* feat: update api

* chore: update proto

* feat: add link impl for appflowy cloud

* feat: impl set and get default view for workspace

* chore: merge with main and update to latest cloud client api

* fix: ci build

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-10-23 19:41:46 +08:00
Lucas faa95ffb9c feat: support toggle heading (#6598)
* feat: support toggle heading

* feat: add numbered list shortcut and heading shortcut

* feat: update format greater to toggle list shortcut

* feat: add removeToggleHeadingStyle shortcut

* feat: add toggle list and toggle headings in turn into menu

* feat: turn into toggle heading block

* feat: highlight selected toggle heading

* fix: collapsed button animation

* test: turn heading block to toggle heading block

* test: turn toggle heading block to heading block

* test: convert '# + >' or '> + #' to toggle heading block

* test: turn heading 2 block with following heading 1 block to toggle heading 2 block

* test: integration tests

* fix: unable to move children after heading 1 to toggle heading block

* test: unable to move children after heading 1 to toggle heading block

* fix: flutter analyze

* fix: toggle heading block test: insert toggle heading 1 - 3 block by shortcuts

* fix: inline database in document tests

* fix: integration test
2024-10-23 19:41:12 +08:00
Richard Shiue 6a5c4c95b7 feat(flutter): date picker improvements (#6582)
* feat(flutter): improve date picker

* test: add widget tests

* test: add bloc test

* test: adjust rust-lib tests

* fix: fix failing tests

* chore: fix rustfmt

* test: remove redundant await

* fix: fix failing tests

* feat: use cupertino date picker on mobile

* fix: flutter analyzer
2024-10-23 08:51:37 +08:00
Richard Shiue 7141e9d8cf fix(flutter-moile): edit primary field from database controls (#6591) 2024-10-22 20:22:15 +08:00
Richard Shiue d7bbb4261c chore: remove legacy CreateRowParams (#6592) 2024-10-22 20:22:03 +08:00
630 changed files with 17801 additions and 8104 deletions

No files matched your search

+21 -78
View File
@@ -28,7 +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
CLOUD_VERSION: 0.6.54-amd64
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -262,11 +262,26 @@ jobs:
else
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
echo "AppFlowy-Cloud is running with an incorrect version. Restarting with the correct version..."
# Remove all containers if any exist
if [ "$(docker ps -aq)" ]; then
docker rm -f $(docker ps -aq)
else
echo "No containers to remove."
fi
# Remove all volumes if any exist
if [ "$(docker volume ls -q)" ]; then
docker volume rm $(docker volume ls -q)
else
echo "No volumes to remove."
fi
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
docker ps -a
docker compose logs
else
echo "AppFlowy-Cloud is running with the correct version."
fi
@@ -324,14 +339,14 @@ jobs:
flutter test integration_test/desktop/cloud/cloud_runner.dart -d Linux --coverage
shell: bash
# split the integration tests into different machines to minimize the time
integration_test_1:
integration_test:
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
test_number: [1, 2, 3, 4, 5, 6, 7, 8]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
@@ -340,82 +355,10 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- name: Flutter Integration Test 1
- name: Flutter Integration Test ${{ matrix.test_number }}
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_1.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
integration_test_2:
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Flutter Integration Test 2
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_2.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
integration_test_3:
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Flutter Integration Test 3
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_3.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
integration_test_4:
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Flutter Integration Test 4
uses: ./.github/actions/flutter_integration_test
with:
test_path: integration_test/desktop_runner_4.dart
test_path: integration_test/desktop_runner_${{ matrix.test_number }}.dart
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
+1 -1
View File
@@ -22,7 +22,7 @@ on:
env:
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.80.1"
CLOUD_VERSION: 0.6.34-amd64
CLOUD_VERSION: 0.6.51
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+21 -18
View File
@@ -20,7 +20,7 @@ on:
env:
CARGO_TERM_COLOR: always
CLOUD_VERSION: 0.6.34-amd64
CLOUD_VERSION: 0.6.51-amd64
RUST_TOOLCHAIN: "1.77.2"
jobs:
@@ -66,6 +66,8 @@ jobs:
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
docker ps -a
docker compose logs
else
echo "AppFlowy-Cloud is running with the correct version."
fi
@@ -145,26 +147,27 @@ jobs:
APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}
APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}
run: |
container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
if [ -z "$container_id" ]; then
echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
# Remove all containers if any exist
if [ "$(docker ps -aq)" ]; then
docker rm -f $(docker ps -aq)
else
running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
else
echo "AppFlowy-Cloud is running with the correct version."
fi
echo "No containers to remove."
fi
# Remove all volumes if any exist
if [ "$(docker volume ls -q)" ]; then
docker volume rm $(docker volume ls -q)
else
echo "No volumes to remove."
fi
docker compose pull
docker compose up -d
echo "Waiting for the container to be ready..."
sleep 10
docker ps -a
docker compose logs
- name: Run rust-lib tests
working-directory: frontend/rust-lib
env:
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.7.2"
APPFLOWY_VERSION = "0.7.3"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database/board/presentation/widgets/board_colum
import 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -121,22 +120,3 @@ void main() {
});
});
}
extension FlowySvgFinder on CommonFinders {
Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg);
}
class _FlowySvgFinder extends MatchFinder {
_FlowySvgFinder(this.svg);
final FlowySvgData svg;
@override
String get description => 'flowy_svg "$svg"';
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
return widget is FlowySvg && widget.svg == svg;
}
}
@@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:time/time.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
@@ -14,6 +15,31 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board row test', () {
testWidgets('edit item in ToDo card', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
const name = 'Card 1';
final card1 = find.ancestor(
matching: find.byType(RowCard),
of: find.text(name),
);
await tester.hoverOnWidget(
card1,
onHover: () async {
final editCard = find.byType(EditCardAccessory);
await tester.tapButton(editCard);
},
);
await tester.showKeyboard(card1);
tester.testTextInput.enterText("");
await tester.pump(300.milliseconds);
tester.testTextInput.enterText("a");
await tester.pump(300.milliseconds);
expect(find.text('a'), findsOneWidget);
});
testWidgets('delete item in ToDo card', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -0,0 +1,157 @@
// ignore_for_file: unused_import
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/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_tab.dart';
import 'package:appflowy/plugins/shared/share/share_menu.dart';
import 'package:appflowy/shared/feature_flags.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/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.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/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Publish:', () {
testWidgets('publish document', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
final publishButton = find.byType(PublishButton);
final unpublishButton = find.byType(UnPublishButton);
await tester.tapButton(publishButton);
// expect to see unpublish, visit site and manage all sites button
expect(unpublishButton, findsOneWidget);
expect(find.text(LocaleKeys.shareAction_visitSite.tr()), findsOneWidget);
// unpublish the document
await tester.tapButton(unpublishButton);
// expect to see publish button
expect(publishButton, findsOneWidget);
});
testWidgets('rename path name', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
final publishButton = find.byType(PublishButton);
await tester.tapButton(publishButton);
// rename the path name
final inputField = find.descendant(
of: find.byType(ShareMenu),
matching: find.byType(TextField),
);
// rename with invalid name
await tester.tap(inputField);
await tester.enterText(inputField, '&&&&????');
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with error message
final errorToast1 = find.text(
LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters
.tr(),
);
await tester.pumpUntilFound(errorToast1);
await tester.pumpUntilNotFound(errorToast1);
// rename with long name
await tester.tap(inputField);
await tester.enterText(inputField, 'long-path-name' * 200);
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with error message
final errorToast2 = find.text(
LocaleKeys.settings_sites_error_publishNameTooLong.tr(),
);
await tester.pumpUntilFound(errorToast2);
await tester.pumpUntilNotFound(errorToast2);
await tester.tap(inputField);
// input the new path name
await tester.enterText(inputField, 'new-path-name');
// click save button
await tester.tapButton(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
// expect to see the toast with success message
final successToast = find.text(
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
await tester.pumpUntilFound(successToast);
await tester.pumpUntilNotFound(successToast);
// click the copy link button
await tester.tapButton(
find.byWidgetPredicate(
(widget) =>
widget is FlowySvg &&
widget.svg.path == FlowySvgs.m_toolbar_link_m.path,
),
);
await tester.pumpAndSettle();
// check the clipboard has the link
final content = await Clipboard.getData(Clipboard.kTextPlain);
expect(
content?.text?.contains('new-path-name'),
isTrue,
);
});
});
}
@@ -3,10 +3,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;
import 'document_publish_test.dart' as document_publish_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
document_option_actions_test.main();
document_copy_link_to_block_test.main();
document_publish_test.main();
}
@@ -6,6 +6,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
import 'package:appflowy/plugins/shared/share/constants.dart';
import 'package:appflowy/plugins/shared/share/share_menu.dart';
import 'package:appflowy/shared/feature_flags.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';
@@ -61,11 +62,11 @@ void main() {
final plainText = clipboardContent.plainText;
expect(
plainText,
startsWith(ShareConstants.shareBaseUrl),
matches(appflowySharePageLinkPattern),
);
final shareValues = plainText!
.replaceAll('${ShareConstants.shareBaseUrl}/', '')
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, isNotEmpty);
@@ -4,8 +4,10 @@ import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/shared/share/publish_tab.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
@@ -15,6 +17,13 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_item.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/home_page_menu.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
@@ -27,6 +36,7 @@ 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/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
@@ -93,4 +103,266 @@ void main() {
},
);
});
group('sites settings:', () {
testWidgets(
'manage published page, set it as homepage, remove the homepage',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
await tester.tapButton(find.byType(PublishButton));
// click empty area to close the publish menu
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
// check if the page is published in sites page
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
// check if the page is published in sites page
final pageItem = find.byWidgetPredicate(
(widget) =>
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
expect(pageItem, findsOneWidget);
// comment it out because it's not allowed to update the namespace in free plan
// // set it to homepage
// await tester.tapButton(
// find.textContaining(
// LocaleKeys.settings_sites_selectHomePage.tr(),
// ),
// );
// await tester.tapButton(
// find.descendant(
// of: find.byType(SelectHomePageMenu),
// matching: find.text(pageName),
// ),
// );
// await tester.pumpAndSettle();
// // check if the page is set to homepage
// final homePageItem = find.descendant(
// of: find.byType(DomainItem),
// matching: find.text(pageName),
// );
// expect(homePageItem, findsOneWidget);
// // remove the homepage
// await tester.tapButton(find.byType(DomainMoreAction));
// await tester.tapButton(
// find.text(LocaleKeys.settings_sites_removeHomepage.tr()),
// );
// await tester.pumpAndSettle();
// // check if the page is removed from homepage
// expect(homePageItem, findsNothing);
});
testWidgets('update namespace', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// check if the page is published in sites page
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
// update the domain
final domainMoreAction = find.byType(DomainMoreAction);
await tester.tapButton(domainMoreAction);
final updateNamespaceButton = find.text(
LocaleKeys.settings_sites_updateNamespace.tr(),
);
await tester.pumpUntilFound(updateNamespaceButton);
// click the update namespace button
await tester.tapButton(updateNamespaceButton);
// comment it out because it's not allowed to update the namespace in free plan
// expect to see the dialog
// await tester.updateNamespace('&&&???');
// // need to upgrade to pro plan to update the namespace
// final errorToast = find.text(
// LocaleKeys.settings_sites_error_proPlanLimitation.tr(),
// );
// await tester.pumpUntilFound(errorToast);
// expect(errorToast, findsOneWidget);
// await tester.pumpUntilNotFound(errorToast);
// comment it out because it's not allowed to update the namespace in free plan
// // short namespace
// await tester.updateNamespace('a');
// // expect to see the toast with error message
// final errorToast2 = find.text(
// LocaleKeys.settings_sites_error_namespaceTooShort.tr(),
// );
// await tester.pumpUntilFound(errorToast2);
// expect(errorToast2, findsOneWidget);
// await tester.pumpUntilNotFound(errorToast2);
// // valid namespace
// await tester.updateNamespace('AppFlowy');
// // expect to see the toast with success message
// final successToast = find.text(
// LocaleKeys.settings_sites_success_namespaceUpdated.tr(),
// );
// await tester.pumpUntilFound(successToast);
// expect(successToast, findsOneWidget);
});
testWidgets('''
More actions for published page:
1. visit site
2. copy link
3. settings
4. unpublish
5. custom url
''', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const pageName = 'Document';
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Document,
pageName: pageName,
);
// open the publish menu
await tester.openPublishMenu();
// publish the document
await tester.tapButton(find.byType(PublishButton));
// click empty area to close the publish menu
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
// check if the page is published in sites page
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
// check if the page is published in sites page
final pageItem = find.byWidgetPredicate(
(widget) =>
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
expect(pageItem, findsOneWidget);
final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());
final customUrlItem = find.text(LocaleKeys.settings_sites_customUrl.tr());
final unpublishItem = find.text(LocaleKeys.shareAction_unPublish.tr());
// custom url
final publishMoreAction = find.byType(PublishedViewMoreAction);
// click the copy link button
{
await tester.tapButton(publishMoreAction);
await tester.pumpAndSettle();
await tester.pumpUntilFound(copyLinkItem);
await tester.tapButton(copyLinkItem);
await tester.pumpAndSettle();
await tester.pumpUntilNotFound(copyLinkItem);
final clipboardContent = await getIt<ClipboardService>().getData();
final plainText = clipboardContent.plainText;
expect(
plainText,
contains(pageName),
);
}
// custom url
{
await tester.tapButton(publishMoreAction);
await tester.pumpAndSettle();
await tester.pumpUntilFound(customUrlItem);
await tester.tapButton(customUrlItem);
await tester.pumpAndSettle();
await tester.pumpUntilNotFound(customUrlItem);
// see the custom url dialog
final customUrlDialog = find.byType(PublishedViewSettingsDialog);
expect(customUrlDialog, findsOneWidget);
// rename the custom url
final textField = find.descendant(
of: customUrlDialog,
matching: find.byType(TextField),
);
await tester.enterText(textField, 'hello-world');
await tester.pumpAndSettle();
// click the save button
final saveButton = find.descendant(
of: customUrlDialog,
matching: find.text(LocaleKeys.button_save.tr()),
);
await tester.tapButton(saveButton);
await tester.pumpAndSettle();
// expect to see the toast with success message
final successToast = find.text(
LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),
);
await tester.pumpUntilFound(successToast);
expect(successToast, findsOneWidget);
}
// unpublish
{
await tester.tapButton(publishMoreAction);
await tester.pumpAndSettle();
await tester.pumpUntilFound(unpublishItem);
await tester.tapButton(unpublishItem);
await tester.pumpAndSettle();
await tester.pumpUntilNotFound(unpublishItem);
// expect to see the toast with success message
final successToast = find.text(
LocaleKeys.publish_unpublishSuccessfully.tr(),
);
await tester.pumpUntilFound(successToast);
expect(successToast, findsOneWidget);
await tester.pumpUntilNotFound(successToast);
// check if the page is unpublished in sites page
expect(pageItem, findsNothing);
}
});
});
}
@@ -211,7 +211,7 @@ void main() {
await tester.toggleIncludeTime();
// Select a date
final now = DateTime.now();
DateTime now = DateTime.now();
await tester.selectDay(content: now.day);
await tester.dismissCellEditor();
@@ -225,7 +225,7 @@ void main() {
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
// Toggle include time
// When toggling include time, the time value is from the previous existing date time, not the current time
now = DateTime.now();
await tester.toggleIncludeTime();
await tester.dismissCellEditor();
@@ -582,7 +582,6 @@ void main() {
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y').format(now),
// content: DateFormat('MMM dd, y HH:mm').format(now),
);
tester.assertCellContent(
rowIndex: 1,
@@ -21,7 +21,6 @@ import 'package:path_provider/path_provider.dart';
import '../../shared/database_test_op.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -69,10 +68,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
await tester.tapFileUploadHint();
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
@@ -85,9 +81,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.tapFileUploadHint();
await tester.pumpAndSettle();
// Expect two files
@@ -139,10 +133,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
await tester.tapFileUploadHint();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
@@ -193,10 +184,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
await tester.tapFileUploadHint();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
@@ -230,7 +218,7 @@ void main() {
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('hide file names', (tester) async {
testWidgets('show file names', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -272,10 +260,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.pumpAndSettle();
await tester.tapFileUploadHint();
// Expect two files
expect(find.byType(RenderMedia), findsNWidgets(2));
@@ -283,28 +268,28 @@ void main() {
await tester.dismissCellEditor();
await tester.pumpAndSettle();
// Open first row in row detail view then toggle hide file names
// Open first row in row detail view then toggle show file names
await tester.openFirstRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to not be shown (hidden)
expect(find.text('sample.jpeg'), findsNothing);
expect(find.text('sample.gif'), findsNothing);
await tester.tapGridFieldWithNameInRowDetailPage('Type');
await tester.pumpAndSettle();
// Toggle show file names
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
// Expect file names to be shown
expect(find.text('sample.jpeg'), findsOneWidget);
expect(find.text('sample.gif'), findsOneWidget);
await tester.tapGridFieldWithNameInRowDetailPage('Type');
await tester.pumpAndSettle();
// Toggle hide file names
await tester.tap(find.byType(Toggle));
await tester.pumpAndSettle();
await tester.dismissRowDetailPage();
await tester.pumpAndSettle();
// Expect file names to be hidden
expect(find.text('sample.jpeg'), findsNothing);
expect(find.text('sample.gif'), findsNothing);
// Remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
@@ -62,9 +62,7 @@ void main() {
await tester.pumpAndSettle();
// Tap on the upload interaction
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
);
await tester.tapFileUploadHint();
// Expect one file
expect(find.byType(RenderMedia), findsOneWidget);
@@ -0,0 +1,20 @@
import 'package:integration_test/integration_test.dart';
import 'database_cell_test.dart' as database_cell_test;
import 'database_field_settings_test.dart' as database_field_settings_test;
import 'database_field_test.dart' as database_field_test;
import 'database_row_page_test.dart' as database_row_page_test;
import 'database_setting_test.dart' as database_setting_test;
import 'database_share_test.dart' as database_share_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_cell_test.main();
database_field_test.main();
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_setting_test.main();
// DON'T add more tests here.
}
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_media_test.dart' as database_media_test;
import 'database_row_cover_test.dart' as database_row_cover_test;
import 'database_share_test.dart' as database_share_test;
import 'database_sort_test.dart' as database_sort_test;
import 'database_view_test.dart' as database_view_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
database_media_test.main();
database_row_cover_test.main();
database_share_test.main();
// DON'T add more tests here.
}
@@ -1,6 +1,5 @@
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';
@@ -14,7 +13,6 @@ 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";
@@ -52,7 +50,6 @@ void main() {
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(
@@ -126,7 +123,6 @@ void main() {
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(
@@ -152,7 +148,6 @@ void main() {
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(
@@ -196,7 +191,6 @@ void main() {
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',
@@ -215,7 +209,6 @@ void main() {
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(
@@ -241,7 +234,6 @@ void main() {
expect(find.text(_createdPageName), findsNWidgets(2));
expect(find.byType(MentionSubPageBlock), findsOneWidget);
expect(find.byFlowySvg(FlowySvgs.child_page_s), findsOneWidget);
// Paste again
await tester.simulateKeyEvent(
@@ -253,7 +245,6 @@ void main() {
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));
});
@@ -272,7 +263,6 @@ void main() {
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();
@@ -284,7 +274,6 @@ void main() {
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]);
@@ -293,7 +282,6 @@ void main() {
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',
@@ -332,7 +320,6 @@ void main() {
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]);
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -73,10 +74,7 @@ void main() {
);
await tester.pumpAndSettle();
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@@ -99,10 +97,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
@@ -155,10 +150,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
@@ -216,10 +208,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
@@ -260,10 +249,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
@@ -313,10 +299,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@@ -326,6 +309,11 @@ void main() {
expect(find.text('Child page'), findsNothing);
expect(find.byType(SubPageBlockComponent), findsNothing);
// Since there is no selection active in editor before deleting Node,
// we need to give focus back to the editor
await tester.editor
.updateSelection(Selection.collapsed(Position(path: [0])));
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
@@ -354,10 +342,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
// Delete
@@ -405,15 +390,16 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
await tester.hoverOnPageName('Child page');
await tester.tapDeletePageButton();
await tester.hoverOnPageName(
'Child page',
onHover: () async {
await tester.tapDeletePageButton();
},
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Child page'), findsNothing);
@@ -432,10 +418,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@@ -473,6 +456,49 @@ void main() {
expect(afterNode.type, beforeNode.type);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
});
testWidgets('turn into page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
final editorState = tester.editor.getCurrentEditorState();
// Insert nested list
final transaction = editorState.transaction;
transaction.insertNode(
[0],
bulletedListNode(
text: 'Parent',
children: [
bulletedListNode(text: 'Child 1'),
bulletedListNode(text: 'Child 2'),
],
),
);
await editorState.apply(transaction);
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsNothing);
await tester.editor.hoverAndClickOptionMenuButton([0]);
await tester.tapButtonWithName(
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
);
await tester.pumpAndSettle();
await tester.tap(find.text(LocaleKeys.editor_page.tr()));
await tester.pumpAndSettle();
expect(find.byType(SubPageBlockComponent), findsOneWidget);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(find.text('Parent'), findsNWidgets(2));
});
});
}
@@ -498,4 +524,16 @@ extension _SubPageTestHelper on WidgetTester {
await pumpUntilFound(find.byType(SubPageBlockComponent));
}
Future<void> renamePageWithSecondary(
String currentName,
String newName,
) async {
await hoverOnPageName(currentName, onHover: () async => pumpAndSettle());
await rightClickOnPageName(currentName);
await tapButtonWithName(ViewMoreActionType.rename.name);
await enterText(find.byType(TextFormField), newName);
await tapOKButton();
await pumpAndSettle();
}
}
@@ -1,26 +1,12 @@
import 'package:integration_test/integration_test.dart';
import 'document_alignment_test.dart' as document_alignment_test;
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
import 'document_create_and_delete_test.dart'
as document_create_and_delete_test;
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_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;
import 'document_with_database_test.dart' as document_with_database_test;
import 'document_with_file_test.dart' as document_with_file_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
void main() {
@@ -33,17 +19,5 @@ void main() {
document_with_inline_page_test.main();
document_with_inline_math_equation_test.main();
document_with_cover_image_test.main();
document_with_outline_block.main();
document_with_toggle_list_test.main();
document_copy_and_paste_test.main();
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_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
// Don't add new tests here.
}
@@ -2,22 +2,25 @@ 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_inline_sub_page_test.dart' as document_inline_sub_page_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;
import 'document_with_toggle_heading_block_test.dart'
as document_with_toggle_heading_block_test;
import 'document_sub_page_test.dart' as document_sub_page_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_title_test.main();
// Disable subPage test temporarily, enable it in version 0.7.2
// document_sub_page_test.main();
document_app_lifecycle_test.main();
document_with_date_reminder_test.main();
document_deletion_test.main();
document_option_action_test.main();
document_inline_sub_page_test.main();
document_with_toggle_heading_block_test.main();
document_sub_page_test.main();
}
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'document_alignment_test.dart' as document_alignment_test;
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
document_with_outline_block.main();
document_with_toggle_list_test.main();
document_copy_and_paste_test.main();
document_codeblock_paste_test.main();
document_alignment_test.main();
document_text_direction_test.main();
// Don't add new tests here.
}
@@ -0,0 +1,25 @@
import 'package:integration_test/integration_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_shortcuts_test.dart' as document_shortcuts_test;
import 'document_with_file_test.dart' as document_with_file_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Document integration tests
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();
// Disable subPage test temporarily, enable it in version 0.7.2
// document_sub_page_test.main();
}
@@ -2,6 +2,7 @@ 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/date_picker/desktop_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -10,7 +11,6 @@ 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(() {
@@ -33,7 +33,6 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
offset: 100,
);
final dateTimeSettings = DateTimeSettingsPB(
@@ -57,13 +56,15 @@ void main() {
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,
);
final textField = find
.descendant(
of: find.byType(DesktopAppFlowyDatePicker),
matching: find.byType(TextField),
)
.last;
await tester.pumpUntilFound(textField);
await tester.enterText(textField, "11:12");
await tester.editor.tapLineOfEditorAt(0);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// we will get field with current date and 11:12 as time
@@ -85,7 +86,6 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
offset: 100,
);
final dateTimeSettings = DateTimeSettingsPB(
@@ -47,9 +47,7 @@ void main() {
mockPickFilePaths(paths: [filePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tap(
find.text(LocaleKeys.document_plugins_file_fileUploadHint.tr()),
);
await tester.tapFileUploadHint();
await tester.pumpAndSettle();
expect(find.byType(FileUploadMenu), findsNothing);
@@ -6,7 +6,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.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/image/multi_image_block_component/multi_image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
@@ -26,7 +25,6 @@ import 'package:path_provider/path_provider.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
setUp(() {
@@ -169,7 +169,6 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_outline.tr(),
offset: 100,
);
await tester.pumpAndSettle();
}
@@ -0,0 +1,128 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
const String _heading1 = 'Heading 1';
const String _heading2 = 'Heading 2';
const String _heading3 = 'Heading 3';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('toggle heading block test:', () {
testWidgets('insert toggle heading 1 - 3 block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'toggle heading block test',
);
for (var i = 1; i <= 3; i++) {
await tester.editor.tapLineOfEditorAt(0);
await _insertToggleHeadingBlockInDocument(tester, i);
await tester.pumpAndSettle();
expect(
find.byWidgetPredicate(
(widget) =>
widget is ToggleListBlockComponentWidget &&
widget.node.attributes[ToggleListBlockKeys.level] == i,
),
findsOneWidget,
);
}
});
testWidgets('insert toggle heading 1 - 3 block by shortcuts',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'toggle heading block test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# > $_heading1\n');
await tester.ime.insertText('## > $_heading2\n');
await tester.ime.insertText('### > $_heading3\n');
await tester.ime.insertText('> # $_heading1\n');
await tester.ime.insertText('> ## $_heading2\n');
await tester.ime.insertText('> ### $_heading3\n');
await tester.pumpAndSettle();
expect(
find.byType(ToggleListBlockComponentWidget),
findsNWidgets(6),
);
});
testWidgets('insert toggle heading and convert it to heading',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'toggle heading block test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# > $_heading1\n');
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.ime.insertText('item 1');
await tester.pumpAndSettle();
await tester.editor.updateSelection(
Selection(
start: Position(path: [0]),
end: Position(path: [0], offset: _heading1.length),
),
);
await tester.tapButton(find.byType(HeadingPopup));
await tester.pumpAndSettle();
expect(
find.byType(HeadingButton),
findsNWidgets(3),
);
// tap the H1 button
await tester.tapButton(find.byType(HeadingButton).at(0));
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();
final node1 = editorState.document.nodeAtPath([0])!;
expect(node1.type, HeadingBlockKeys.type);
expect(node1.attributes[HeadingBlockKeys.level], 1);
final node2 = editorState.document.nodeAtPath([1])!;
expect(node2.type, ParagraphBlockKeys.type);
expect(node2.delta!.toPlainText(), 'item 1');
});
});
}
Future<void> _insertToggleHeadingBlockInDocument(
WidgetTester tester,
int level,
) async {
final name = switch (level) {
1 => LocaleKeys.document_slashMenu_name_toggleHeading1.tr(),
2 => LocaleKeys.document_slashMenu_name_toggleHeading2.tr(),
3 => LocaleKeys.document_slashMenu_name_toggleHeading3.tr(),
_ => throw Exception('Invalid level: $level'),
};
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
name,
offset: 150,
);
await tester.pumpAndSettle();
}
@@ -214,5 +214,54 @@ void main() {
expectToggleListOpened();
});
Future<void> prepareToggleHeadingBlock(
WidgetTester tester,
String text,
) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(text);
}
testWidgets('> + # to toggle heading 1 block', (tester) async {
await prepareToggleHeadingBlock(tester, '> # Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 1);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('> + ### to toggle heading 3 block', (tester) async {
await prepareToggleHeadingBlock(tester, '> ### Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 3);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('# + > to toggle heading 1 block', (tester) async {
await prepareToggleHeadingBlock(tester, '# > Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 1);
expect(node.delta!.toPlainText(), 'Hello');
});
testWidgets('### + > to toggle heading 3 block', (tester) async {
await prepareToggleHeadingBlock(tester, '### > Hello');
final editorState = tester.editor.getCurrentEditorState();
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ToggleListBlockKeys.type);
expect(node.attributes[ToggleListBlockKeys.level], 3);
expect(node.delta!.toPlainText(), 'Hello');
});
});
}
@@ -11,7 +11,7 @@ void main() {
testWidgets('empty test', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.wait(1000);
await tester.wait(500);
});
});
}
@@ -0,0 +1,19 @@
import 'package:integration_test/integration_test.dart';
import 'grid_edit_row_test.dart' as grid_edit_row_test_runner;
import 'grid_filter_and_sort_test.dart' as grid_filter_and_sort_test_runner;
import 'grid_reopen_test.dart' as grid_reopen_test_runner;
import 'grid_reorder_row_test.dart' as grid_reorder_row_test_runner;
import 'grid_row_test.dart' as grid_row_test_runner;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
grid_reopen_test_runner.main();
grid_row_test_runner.main();
grid_reorder_row_test_runner.main();
grid_filter_and_sort_test_runner.main();
grid_edit_row_test_runner.main();
// grid_calculations_test_runner.main();
// DON'T add more tests here.
}
@@ -10,7 +10,6 @@ import 'package:integration_test/integration_test.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -19,8 +19,13 @@ void main() {
await tester.tapAnonymousSignInButton();
// Right click on the view item and change icon
await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);
await tester.pumpAndSettle();
await tester.hoverOnWidget(
find.byType(ViewItem),
onHover: () async {
await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);
await tester.pumpAndSettle();
},
);
// Change icon
final changeIconButton =
@@ -0,0 +1,23 @@
import 'package:integration_test/integration_test.dart';
import 'emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'hotkeys_test.dart' as hotkeys_test;
import 'import_files_test.dart' as import_files_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'tabs_test.dart' as tabs_test;
import 'zoom_in_out_test.dart' as zoom_in_out_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
hotkeys_test.main();
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
share_markdown_test.main();
import_files_test.main();
tabs_test.main();
zoom_in_out_test.main();
// DON'T add more tests here.
}
@@ -1,7 +1,6 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_1.dart' as document_test_runner;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/document/document_test_runner_1.dart' as document_test_runner_1;
import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test;
Future<void> main() async {
@@ -11,11 +10,7 @@ Future<void> main() async {
Future<void> runIntegration1OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
switch_folder_test.main();
document_test_runner.main();
document_test_runner_1.main();
// DON'T add more tests here.
}
@@ -1,18 +1,7 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/database/database_calendar_test.dart' as database_calendar_test;
import 'desktop/database/database_cell_test.dart' as database_cell_test;
import 'desktop/database/database_field_settings_test.dart'
as database_field_settings_test;
import 'desktop/database/database_field_test.dart' as database_field_test;
import 'desktop/database/database_filter_test.dart' as database_filter_test;
import 'desktop/database/database_media_test.dart' as database_media_test;
import 'desktop/database/database_row_page_test.dart' as database_row_page_test;
import 'desktop/database/database_setting_test.dart' as database_setting_test;
import 'desktop/database/database_share_test.dart' as database_share_test;
import 'desktop/database/database_sort_test.dart' as database_sort_test;
import 'desktop/database/database_view_test.dart' as database_view_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/database/database_test_runner_1.dart' as database_test_runner_1;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration2OnDesktop();
@@ -21,20 +10,8 @@ Future<void> main() async {
Future<void> runIntegration2OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
database_cell_test.main();
database_field_test.main();
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_setting_test.main();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
database_media_test.main();
database_test_runner_1.main();
// DON'T add more tests here. This is the second test runner for desktop.
}
@@ -1,24 +1,8 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/database/database_row_cover_test.dart'
as database_row_cover_test;
import 'desktop/grid/grid_edit_row_test.dart' as grid_edit_row_test_runner;
import 'desktop/grid/grid_filter_and_sort_test.dart'
as grid_filter_and_sort_test_runner;
import 'desktop/grid/grid_reopen_test.dart' as grid_reopen_test_runner;
import 'desktop/grid/grid_reorder_row_test.dart'
as grid_reorder_row_test_runner;
import 'desktop/grid/grid_row_test.dart' as grid_create_row_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
import 'desktop/uncategorized/import_files_test.dart' as import_files_test;
import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/uncategorized/zoom_in_out_test.dart' as zoom_in_out_test;
import 'desktop/first_test/first_test.dart' as first_test;
import 'desktop/grid/grid_test_runner_1.dart' as grid_test_runner_1;
Future<void> main() async {
await runIntegration3OnDesktop();
@@ -27,26 +11,9 @@ Future<void> main() async {
Future<void> runIntegration3OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
settings_test_runner.main();
share_markdown_test.main();
import_files_test.main();
sidebar_test_runner.main();
board_test_runner.main();
tabs_test.main();
database_row_cover_test.main();
grid_reopen_test_runner.main();
grid_create_row_test_runner.main();
grid_reorder_row_test_runner.main();
grid_filter_and_sort_test_runner.main();
grid_edit_row_test_runner.main();
zoom_in_out_test.main();
// DON'T add more tests here. Add them to desktop_runner_4.dart
grid_test_runner_1.main();
// DON'T add more tests here.
}
@@ -1,7 +1,7 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_2.dart' as document_test_runner_2;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration4OnDesktop();
@@ -10,8 +10,8 @@ Future<void> main() async {
Future<void> runIntegration4OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
document_test_runner_2.main();
// DON'T add more tests here.
}
@@ -0,0 +1,17 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/database/database_test_runner_2.dart' as database_test_runner_2;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration5OnDesktop();
}
Future<void> runIntegration5OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
database_test_runner_2.main();
// DON'T add more tests here.
}
@@ -0,0 +1,22 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/first_test/first_test.dart' as first_test;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/uncategorized_test_runner_1.dart'
as uncategorized_test_runner_1;
Future<void> main() async {
await runIntegration6OnDesktop();
}
Future<void> runIntegration6OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
settings_test_runner.main();
sidebar_test_runner.main();
uncategorized_test_runner_1.main();
// DON'T add more tests here.
}
@@ -0,0 +1,17 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_3.dart' as document_test_runner_3;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration7OnDesktop();
}
Future<void> runIntegration7OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
document_test_runner_3.main();
// DON'T add more tests here.
}
@@ -0,0 +1,17 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner_4.dart' as document_test_runner_4;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration8OnDesktop();
}
Future<void> runIntegration8OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
document_test_runner_4.main();
// DON'T add more tests here.
}
@@ -4,6 +4,10 @@ import 'desktop_runner_1.dart';
import 'desktop_runner_2.dart';
import 'desktop_runner_3.dart';
import 'desktop_runner_4.dart';
import 'desktop_runner_5.dart';
import 'desktop_runner_6.dart';
import 'desktop_runner_7.dart';
import 'desktop_runner_8.dart';
import 'mobile_runner.dart';
/// The main task runner for all integration tests in AppFlowy.
@@ -19,6 +23,10 @@ Future<void> main() async {
await runIntegration2OnDesktop();
await runIntegration3OnDesktop();
await runIntegration4OnDesktop();
await runIntegration5OnDesktop();
await runIntegration6OnDesktop();
await runIntegration7OnDesktop();
await runIntegration8OnDesktop();
} else if (Platform.isIOS || Platform.isAndroid) {
await runIntegrationOnMobile();
} else {
@@ -43,7 +43,6 @@ 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';
@@ -193,8 +192,13 @@ extension CommonOperations on WidgetTester {
ViewLayoutPB layout = ViewLayoutPB.Document,
}) async {
final page = findPageName(name, layout: layout);
await tap(page, buttons: kSecondaryMouseButton);
await pumpAndSettle();
await hoverOnPageName(
name,
onHover: () async {
await tap(page, buttons: kSecondaryMouseButton);
await pumpAndSettle();
},
);
}
/// open the page with given name.
@@ -215,7 +219,7 @@ extension CommonOperations on WidgetTester {
of: find.byType(ViewMoreActionPopover),
matching: find.byFlowySvg(FlowySvgs.workspace_three_dots_s),
);
await tapButton(optionButton, warnIfMissed: true);
await tapButton(optionButton);
}
/// Tap the delete page button.
@@ -312,6 +316,15 @@ extension CommonOperations on WidgetTester {
await tapButton(shareButton);
}
// open the share menu and then click the publish tab
Future<void> openPublishMenu() async {
await tapShareButton();
final publishButton = find.textContaining(
LocaleKeys.shareAction_publishTab.tr(),
);
await tapButton(publishButton);
}
/// Tap the export markdown button
///
/// Must call [tapShareButton] first.
@@ -762,6 +775,18 @@ extension CommonOperations on WidgetTester {
await tap(button);
await pump();
}
Future<void> tapFileUploadHint() async {
final finder = find.byWidgetPredicate(
(w) =>
w is RichText &&
w.text.toPlainText().contains(
LocaleKeys.document_plugins_file_fileUploadHint.tr(),
),
);
await tap(finder);
await pumpAndSettle(const Duration(seconds: 2));
}
}
extension SettingsFinder on CommonFinders {
@@ -790,6 +815,25 @@ extension SettingsFinder on CommonFinders {
.first;
}
extension FlowySvgFinder on CommonFinders {
Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg);
}
class _FlowySvgFinder extends MatchFinder {
_FlowySvgFinder(this.svg);
final FlowySvgData svg;
@override
String get description => 'flowy_svg "$svg"';
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
return widget is FlowySvg && widget.svg == svg;
}
}
extension ViewLayoutPBTest on ViewLayoutPB {
String get menuName {
switch (this) {
@@ -55,7 +55,7 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/ti
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
@@ -596,7 +596,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
final banner = find.byType(RowBanner);
expect(banner, findsOneWidget);
await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse);
await startGesture(
getCenter(banner) + const Offset(0, -10),
kind: PointerDeviceKind.mouse,
);
await pumpAndSettle();
}
@@ -608,7 +611,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButtonWithName(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
await pumpAndSettle();
}
Future<void> openEmojiPicker() async =>
@@ -4,14 +4,15 @@ import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
import 'common_operations.dart';
@@ -117,4 +118,20 @@ extension AppFlowySettings on WidgetTester {
await tapAt(Offset.zero);
await pumpAndSettle();
}
Future<void> updateNamespace(String namespace) async {
final dialog = find.byType(DomainSettingsDialog);
expect(dialog, findsOneWidget);
// input the new namespace
await enterText(
find.descendant(
of: dialog,
matching: find.byType(TextField),
),
namespace,
);
await tapButton(find.text(LocaleKeys.button_save.tr()));
await pumpAndSettle();
}
}
@@ -1,9 +1,9 @@
import 'dart:async';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:flutter/services.dart';
class NetworkListener {
@@ -20,6 +20,7 @@ extension MobileRouter on BuildContext {
bool addInRecent = true,
bool showMoreButton = true,
String? fixedTitle,
String? blockId,
}) async {
// set the current view before pushing the new view
getIt<MenuSharedState>().latestOpenView = view;
@@ -32,6 +33,9 @@ extension MobileRouter on BuildContext {
if (fixedTitle != null) {
queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle;
}
if (blockId != null) {
queryParameters[MobileDocumentScreen.viewBlockId] = blockId;
}
}
final uri = Uri(
@@ -61,6 +61,7 @@ class NotificationReminderBloc
reminderContent: node.delta?.toPlainText() ?? '',
nodes: [node],
status: NotificationReminderStatus.loaded,
blockId: reminder.meta[ReminderMetaKeys.blockId],
),
);
}
@@ -205,6 +206,7 @@ class NotificationReminderState with _$NotificationReminderState {
@Default(NotificationReminderStatus.initial)
NotificationReminderStatus status,
@Default([]) List<Node> nodes,
String? blockId,
ViewPB? view,
}) = _NotificationReminderState;
@@ -30,6 +30,7 @@ class MobileViewPage extends StatefulWidget {
this.arguments,
this.fixedTitle,
this.showMoreButton = true,
this.blockId,
});
/// view id
@@ -38,6 +39,7 @@ class MobileViewPage extends StatefulWidget {
final String? title;
final Map<String, dynamic>? arguments;
final bool showMoreButton;
final String? blockId;
// only used in row page
final String? fixedTitle;
@@ -63,6 +65,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
@override
void dispose() {
_appBarOpacity.dispose();
_scrollNotificationObserver?.removeListener(_onScrollNotification);
_scrollNotificationObserver = null;
super.dispose();
@@ -177,6 +180,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
context: PluginContext(userProfile: state.userProfilePB),
data: {
MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
MobileDocumentScreen.viewBlockId: widget.blockId,
},
);
},
@@ -9,7 +9,6 @@ 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';
@@ -24,69 +23,75 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
const MobileViewPageMoreBottomSheet({super.key, required this.view});
final ViewPB view;
@override
Widget build(BuildContext context) {
return ViewPageBottomSheet(
view: view,
onAction: (action) async {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate());
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
context.popToHome();
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
context.pop();
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// 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();
return BlocListener<ViewBloc, ViewState>(
listener: (context, state) {
if (state.successOrFailure.isSuccess && state.isDeleted) {
context.go('/home');
}
},
onRename: (name) {
_onRename(context, name);
context.pop();
},
child: ViewPageBottomSheet(
view: view,
onAction: (action) async {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate());
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
context.pop();
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
context.pop();
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.publish:
await _publish(context);
if (context.mounted) {
context.pop();
}
break;
case MobileViewBottomSheetBodyAction.unpublish:
_unpublish(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyPublishLink:
_copyPublishLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.visitSite:
_visitPublishedSite(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.copyShareLink:
_copyShareLink(context);
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
_onRename(context, name);
context.pop();
},
),
);
}
@@ -0,0 +1,96 @@
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart';
import 'package:appflowy/util/xfile_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pbenum.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class MobileMediaUploadSheetContent extends StatelessWidget {
const MobileMediaUploadSheetContent({super.key, required this.dialogContext});
final BuildContext dialogContext;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 12),
constraints: const BoxConstraints(
maxHeight: 340,
minHeight: 80,
),
child: MobileFileUploadMenu(
onInsertLocalFile: (files) async {
dialogContext.pop();
await insertLocalFiles(
context,
files,
userProfile: context.read<MediaCellBloc>().state.userProfile,
documentId: context.read<MediaCellBloc>().rowId,
onUploadSuccess: (file, path, isLocalMode) {
final mediaCellBloc = context.read<MediaCellBloc>();
if (mediaCellBloc.isClosed) {
return;
}
mediaCellBloc.add(
MediaCellEvent.addFile(
url: path,
name: file.name,
uploadType: isLocalMode
? FileUploadTypePB.LocalFile
: FileUploadTypePB.CloudFile,
fileType: file.fileType.toMediaFileTypePB(),
),
);
},
);
},
onInsertNetworkFile: (url) async => _onInsertNetworkFile(
url,
dialogContext,
context,
),
),
);
}
Future<void> _onInsertNetworkFile(
String url,
BuildContext dialogContext,
BuildContext context,
) async {
dialogContext.pop();
if (url.isEmpty) return;
final uri = Uri.tryParse(url);
if (uri == null) {
return;
}
final fakeFile = XFile(uri.path);
MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB();
fileType =
fileType == MediaFileTypePB.Other ? MediaFileTypePB.Link : fileType;
String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : "";
if (name.isEmpty && uri.pathSegments.length > 1) {
name = uri.pathSegments[uri.pathSegments.length - 2];
} else if (name.isEmpty) {
name = uri.host;
}
context.read<MediaCellBloc>().add(
MediaCellEvent.addFile(
url: url,
name: name,
uploadType: FileUploadTypePB.NetworkFile,
fileType: fileType,
),
);
}
}
@@ -143,6 +143,7 @@ Future<T?> showMobileBottomSheet<T>(
) ??
Expanded(
child: Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: child,
@@ -11,12 +11,16 @@ class OptionTextField extends StatelessWidget {
const OptionTextField({
super.key,
required this.controller,
this.autoFocus = false,
required this.isPrimary,
required this.fieldType,
required this.onTextChanged,
required this.onFieldTypeChanged,
});
final TextEditingController controller;
final bool autoFocus;
final bool isPrimary;
final FieldType fieldType;
final void Function(String value) onTextChanged;
final void Function(FieldType value) onFieldTypeChanged;
@@ -25,10 +29,14 @@ class OptionTextField extends StatelessWidget {
Widget build(BuildContext context) {
return FlowyOptionTile.textField(
controller: controller,
autofocus: autoFocus,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),
onTextChanged: onTextChanged,
leftIcon: GestureDetector(
onTap: () async {
if (isPrimary) {
return;
}
final fieldType = await showFieldTypeGridBottomSheet(
context,
title: LocaleKeys.grid_field_editProperty.tr(),
@@ -5,9 +5,8 @@ import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -71,71 +70,52 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
);
}
Widget _buildDatePicker() => MultiBlocProvider(
providers: [
BlocProvider<DateCellEditorBloc>(
create: (_) => DateCellEditorBloc(
reminderBloc: getIt<ReminderBloc>(),
cellController: widget.controller,
),
),
],
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
return MobileAppFlowyDatePicker(
selectedDay: state.dateTime,
dateStr: state.dateStr,
endDateStr: state.endDateStr,
timeStr: state.timeStr,
endTimeStr: state.endTimeStr,
startDay: state.startDay,
endDay: state.endDay,
enableRanges: true,
isRange: state.isRange,
includeTime: state.includeTime,
use24hFormat: state.dateTypeOptionPB.timeFormat ==
TimeFormatPB.TwentyFourHour,
timeFormat: state.dateTypeOptionPB.timeFormat,
selectedReminderOption: state.reminderOption,
onStartTimeChanged: (String? time) {
if (time != null) {
context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setTime(time));
}
},
onEndTimeChanged: (String? time) {
if (time != null) {
context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setEndTime(time));
}
},
onDaySelected: (selectedDay, focusedDay) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.selectDay(selectedDay)),
onRangeSelected: (start, end, focused) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.selectDateRange(start, end)),
onRangeChanged: (value) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIsRange(value)),
onIncludeTimeChanged: (value) => context
.read<DateCellEditorBloc>()
.add(DateCellEditorEvent.setIncludeTime(value)),
onClearDate: () => context
.read<DateCellEditorBloc>()
.add(const DateCellEditorEvent.clearDate()),
onReminderSelected: (option) =>
context.read<DateCellEditorBloc>().add(
DateCellEditorEvent.setReminderOption(
option: option,
selectedDay:
state.dateTime == null ? DateTime.now() : null,
),
),
);
},
),
);
Widget _buildDatePicker() {
return BlocProvider(
create: (_) => DateCellEditorBloc(
reminderBloc: getIt<ReminderBloc>(),
cellController: widget.controller,
),
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
final dateCellBloc = context.read<DateCellEditorBloc>();
return MobileAppFlowyDatePicker(
dateTime: state.dateTime,
endDateTime: state.endDateTime,
isRange: state.isRange,
includeTime: state.includeTime,
dateFormat: state.dateTypeOptionPB.dateFormat,
timeFormat: state.dateTypeOptionPB.timeFormat,
reminderOption: state.reminderOption,
onDaySelected: (selectedDay) {
dateCellBloc.add(DateCellEditorEvent.updateDateTime(selectedDay));
},
onRangeSelected: (start, end) {
dateCellBloc.add(DateCellEditorEvent.updateDateRange(start, end));
},
onIsRangeChanged: (value, dateTime, endDateTime) {
dateCellBloc.add(
DateCellEditorEvent.setIsRange(value, dateTime, endDateTime),
);
},
onIncludeTimeChanged: (value, dateTime, endDateTime) {
dateCellBloc.add(
DateCellEditorEvent.setIncludeTime(
value,
dateTime,
endDateTime,
),
);
},
onClearDate: () {
dateCellBloc.add(const DateCellEditorEvent.clearDate());
},
onReminderSelected: (option) {
dateCellBloc.add(DateCellEditorEvent.setReminderOption(option));
},
);
},
),
);
}
}
@@ -21,15 +21,15 @@ import 'mobile_quick_field_editor.dart';
const mobileSupportedFieldTypes = [
FieldType.RichText,
FieldType.Number,
FieldType.URL,
FieldType.SingleSelect,
FieldType.MultiSelect,
FieldType.DateTime,
FieldType.LastEditedTime,
FieldType.CreatedTime,
FieldType.Media,
FieldType.URL,
FieldType.Checkbox,
FieldType.Checklist,
FieldType.Media,
FieldType.LastEditedTime,
FieldType.CreatedTime,
// FieldType.Time,
];
@@ -224,7 +224,9 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
const _Divider(),
OptionTextField(
controller: controller,
autoFocus: widget.mode == FieldOptionMode.add,
fieldType: values.type,
isPrimary: widget.isPrimary,
onTextChanged: (value) {
isFieldNameChanged = true;
_updateOptionValues(name: value);
@@ -76,6 +76,7 @@ class _QuickEditFieldState extends State<QuickEditField> {
const VSpace(16),
OptionTextField(
controller: controller,
isPrimary: state.field.isPrimary,
fieldType: state.field.fieldType,
onTextChanged: (text) {
context
@@ -13,7 +13,6 @@ import 'package:appflowy/plugins/database/widgets/setting/field_visibility_exten
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:collection/collection.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';
@@ -79,14 +78,12 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
viewId: viewId,
fieldController: databaseController.fieldController,
fieldInfo: field,
index: index,
showTopBorder: false,
),
)
.toList();
return ReorderableListView.builder(
padding: EdgeInsets.zero,
proxyDecorator: (_, index, anim) {
final field = fields[index];
return AnimatedBuilder(
@@ -103,7 +100,6 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
viewId: viewId,
fieldController: databaseController.fieldController,
fieldInfo: field,
index: index,
showTopBorder: true,
),
),
@@ -121,43 +117,32 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
},
header: firstCell,
footer: canCreate
? Column(
mainAxisSize: MainAxisSize.min,
children: [
_divider(),
_NewDatabaseFieldTile(viewId: viewId),
VSpace(
context.bottomSheetPadding(
ignoreViewPadding: false,
),
),
],
? Padding(
padding: const EdgeInsets.only(top: 20),
child: _NewDatabaseFieldTile(viewId: viewId),
)
: VSpace(
context.bottomSheetPadding(ignoreViewPadding: false),
),
: null,
itemCount: cells.length,
itemBuilder: (context, index) => cells[index],
padding: EdgeInsets.only(
bottom: context.bottomSheetPadding(ignoreViewPadding: false),
),
);
},
),
);
}
Widget _divider() => const VSpace(20);
}
class DatabaseFieldListTile extends StatelessWidget {
const DatabaseFieldListTile({
super.key,
this.index,
required this.fieldInfo,
required this.viewId,
required this.fieldController,
required this.showTopBorder,
});
final int? index;
final FieldInfo fieldInfo;
final String viewId;
final FieldController fieldController;
@@ -172,6 +157,7 @@ class DatabaseFieldListTile extends StatelessWidget {
fieldInfo: fieldInfo,
dimension: 20,
),
onTap: () => showEditFieldScreen(context, viewId, fieldInfo),
showTopBorder: showTopBorder,
);
} else {
@@ -1054,34 +1054,55 @@ class _SelectOptionFilterContentEditorState
}
}
class _DateTimeFilterContentEditor extends StatelessWidget {
class _DateTimeFilterContentEditor extends StatefulWidget {
const _DateTimeFilterContentEditor({
required this.filter,
});
final DateTimeFilter filter;
@override
State<_DateTimeFilterContentEditor> createState() =>
_DateTimeFilterContentEditorState();
}
class _DateTimeFilterContentEditorState
extends State<_DateTimeFilterContentEditor> {
late DateTime focusedDay;
bool get isRange => widget.filter.condition.isRange;
@override
void initState() {
super.initState();
focusedDay = (isRange ? widget.filter.start : widget.filter.timestamp) ??
DateTime.now();
}
@override
Widget build(BuildContext context) {
final isRange = filter.condition.isRange;
return MobileDatePicker(
isRange: isRange,
selectedDay: isRange ? filter.start : filter.timestamp,
startDay: isRange ? filter.start : null,
endDay: isRange ? filter.end : null,
onDaySelected: (selectedDay, _) {
selectedDay: isRange ? widget.filter.start : widget.filter.timestamp,
startDay: isRange ? widget.filter.start : null,
endDay: isRange ? widget.filter.end : null,
focusedDay: focusedDay,
onDaySelected: (selectedDay) {
final newFilter = isRange
? filter.copyWithRange(start: selectedDay, end: null)
: filter.copyWithTimestamp(timestamp: selectedDay);
? widget.filter.copyWithRange(start: selectedDay, end: null)
: widget.filter.copyWithTimestamp(timestamp: selectedDay);
context.read<MobileFilterEditorCubit>().updateFilter(newFilter);
},
onRangeSelected: (start, end, _) {
final newFilter = filter.copyWithRange(
onRangeSelected: (start, end) {
final newFilter = widget.filter.copyWithRange(
start: start,
end: end,
);
context.read<MobileFilterEditorCubit>().updateFilter(newFilter);
},
onPageChanged: (focusedDay) {
setState(() => this.focusedDay = focusedDay);
},
);
}
}
@@ -9,6 +9,7 @@ class MobileDocumentScreen extends StatelessWidget {
this.title,
this.showMoreButton = true,
this.fixedTitle,
this.blockId,
});
/// view id
@@ -16,12 +17,14 @@ class MobileDocumentScreen extends StatelessWidget {
final String? title;
final bool showMoreButton;
final String? fixedTitle;
final String? blockId;
static const routeName = '/docs';
static const viewId = 'id';
static const viewTitle = 'title';
static const viewShowMoreButton = 'show_more_button';
static const viewFixedTitle = 'fixed_title';
static const viewBlockId = 'block_id';
@override
Widget build(BuildContext context) {
@@ -31,6 +34,7 @@ class MobileDocumentScreen extends StatelessWidget {
viewLayout: ViewLayoutPB.Document,
showMoreButton: showMoreButton,
fixedTitle: fixedTitle,
blockId: blockId,
);
}
}
@@ -138,11 +138,9 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
if (tabController == null) {
return;
}
context.read<SpaceOrderBloc>().add(
SpaceOrderEvent.open(
tabController!.index,
),
);
context
.read<SpaceOrderBloc>()
.add(SpaceOrderEvent.open(tabController!.index));
}
List<Widget> _buildTabs(SpaceOrderState state) {
@@ -174,13 +172,9 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
}
// quick create new page when clicking the add button in navigation bar
void _createNewDocument() {
_createNewPage(ViewLayoutPB.Document);
}
void _createNewDocument() => _createNewPage(ViewLayoutPB.Document);
void _createNewAIChat() {
_createNewPage(ViewLayoutPB.Chat);
}
void _createNewAIChat() => _createNewPage(ViewLayoutPB.Chat);
void _createNewPage(ViewLayoutPB layout) {
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
@@ -206,8 +200,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId;
if (workspaceId == null) {
Log.error('Workspace ID is null');
return;
return Log.error('Workspace ID is null');
}
context
.read<UserWorkspaceBloc>()
@@ -28,8 +28,8 @@ class MobileNotificationsScreen extends StatefulWidget {
class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
with SingleTickerProviderStateMixin {
final ReminderBloc _reminderBloc = getIt<ReminderBloc>();
late final TabController _controller = TabController(length: 2, vsync: this);
final ReminderBloc reminderBloc = getIt<ReminderBloc>();
late final TabController controller = TabController(length: 2, vsync: this);
@override
Widget build(BuildContext context) {
@@ -39,7 +39,7 @@ class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
create: (context) =>
UserProfileBloc()..add(const UserProfileEvent.started()),
),
BlocProvider<ReminderBloc>.value(value: _reminderBloc),
BlocProvider<ReminderBloc>.value(value: reminderBloc),
BlocProvider<NotificationFilterBloc>(
create: (_) => NotificationFilterBloc(),
),
@@ -54,8 +54,8 @@ class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
_NotificationScreenContent(
workspaceSetting: workspaceSetting,
userProfile: userProfile,
controller: _controller,
reminderBloc: _reminderBloc,
controller: controller,
reminderBloc: reminderBloc,
),
);
},
@@ -124,7 +124,6 @@ class _NotificationScreenContent extends StatelessWidget {
reminderBloc: reminderBloc,
views: sectionState.section.publicViews,
onAction: _onAction,
onDelete: _onDelete,
onReadChanged: _onReadChanged,
actionBar: InboxActionBar(
hasUnreads: state.hasUnreads,
@@ -161,9 +160,6 @@ class _NotificationScreenContent extends StatelessWidget {
),
);
void _onDelete(ReminderPB reminder) =>
reminderBloc.add(ReminderEvent.remove(reminderId: reminder.id));
void _onReadChanged(ReminderPB reminder, bool isRead) => reminderBloc.add(
ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),
);
@@ -61,15 +61,11 @@ class _MobileNotificationsScreenV2State
);
}
void _onRefresh() {
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
}
void _onRefresh() => getIt<ReminderBloc>().add(const ReminderEvent.refresh());
}
class MobileNotificationsTab extends StatefulWidget {
const MobileNotificationsTab({
super.key,
});
const MobileNotificationsTab({super.key});
@override
State<MobileNotificationsTab> createState() => _MobileNotificationsTabState();
@@ -88,17 +84,12 @@ class _MobileNotificationsTabState extends State<MobileNotificationsTab>
@override
void initState() {
super.initState();
tabController = TabController(
length: 3,
vsync: this,
);
tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
@@ -21,6 +21,12 @@ class _MobileNotificationTabBarState extends State<MobileNotificationTabBar> {
widget.controller.addListener(_updateState);
}
@override
void dispose() {
widget.controller.removeListener(_updateState);
super.dispose();
}
void _updateState() => setState(() {});
@override
@@ -64,11 +64,15 @@ class NotificationItem extends StatelessWidget {
child: child,
onTapUp: () async {
final view = state.view;
final blockId = state.blockId;
if (view == null) {
return;
}
await context.pushView(view);
await context.pushView(
view,
blockId: blockId,
);
if (!reminder.isRead && context.mounted) {
context.read<ReminderBloc>().add(
@@ -98,12 +98,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
required bool shrinkWrap,
Map<String, dynamic>? data,
}) {
notifier.isDeleted.addListener(() {
final deletedView = notifier.isDeleted.value;
if (deletedView != null && deletedView.hasIndex()) {
deletedViewIndex = deletedView.index;
}
});
notifier.isDeleted.addListener(_onDeleted);
if (context.userProfile == null) {
Log.error("User profile is null when opening AI Chat plugin");
@@ -122,6 +117,13 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
);
}
void _onDeleted() {
final deletedView = notifier.isDeleted.value;
if (deletedView != null && deletedView.hasIndex()) {
deletedViewIndex = deletedView.index;
}
}
@override
List<NavigationItem> get navigationItems => [this];
@@ -86,9 +86,7 @@ class _ChatInputState extends State<ChatInput> {
},
);
_inputFocusNode.addListener(() {
setState(() {});
});
_inputFocusNode.addListener(_updateState);
_inputActionControl = ChatInputActionControl(
chatId: widget.chatId,
@@ -101,12 +99,15 @@ class _ChatInputState extends State<ChatInput> {
@override
void dispose() {
_inputFocusNode.removeListener(_updateState);
_inputFocusNode.dispose();
_textController.dispose();
_inputActionControl.dispose();
super.dispose();
}
void _updateState() => setState(() {});
@override
Widget build(BuildContext context) {
return Padding(
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
@@ -6,6 +7,8 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'date_cell_editor_bloc.dart';
part 'date_cell_bloc.freezed.dart';
class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
@@ -16,7 +19,7 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
}
final DateCellController cellController;
void Function()? _onCellChangedFn;
VoidCallback? _onCellChangedFn;
@override
Future<void> close() async {
@@ -35,15 +38,19 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
(event, emit) async {
event.when(
didReceiveCellUpdate: (DateCellDataPB? cellData) {
final dateCellData = DateCellData.fromPB(cellData);
emit(
state.copyWith(
data: cellData,
dateStr: _dateStrFromCellData(cellData),
cellData: dateCellData,
),
);
},
didUpdateField: (fieldInfo) {
emit(state.copyWith(fieldInfo: fieldInfo));
emit(
state.copyWith(
fieldInfo: fieldInfo,
),
);
},
);
},
@@ -79,41 +86,16 @@ class DateCellEvent with _$DateCellEvent {
@freezed
class DateCellState with _$DateCellState {
const factory DateCellState({
required DateCellDataPB? data,
required String dateStr,
required FieldInfo fieldInfo,
required DateCellData cellData,
}) = _DateCellState;
factory DateCellState.initial(DateCellController context) {
final cellData = context.getCellData();
factory DateCellState.initial(DateCellController cellController) {
final cellData = DateCellData.fromPB(cellController.getCellData());
return DateCellState(
fieldInfo: context.fieldInfo,
data: cellData,
dateStr: _dateStrFromCellData(cellData),
fieldInfo: cellController.fieldInfo,
cellData: cellData,
);
}
}
String _dateStrFromCellData(DateCellDataPB? cellData) {
if (cellData == null || !cellData.hasTimestamp()) {
return "";
}
String dateStr = "";
if (cellData.isRange) {
if (cellData.includeTime) {
dateStr =
"${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}";
} else {
dateStr = "${cellData.date} → ${cellData.endDate}";
}
} else {
if (cellData.includeTime) {
dateStr = "${cellData.date} ${cellData.time}";
} else {
dateStr = cellData.date;
}
}
return dateStr.trim();
}
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/domain/date_cell_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
@@ -11,18 +12,16 @@ import 'package:appflowy/util/int64_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'
show StringTranslateExtension;
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nanoid/non_secure.dart';
import 'package:protobuf/protobuf.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
part 'date_cell_editor_bloc.freezed.dart';
@@ -45,6 +44,7 @@ class DateCellEditorBloc
final DateCellBackendService _dateCellBackendService;
final DateCellController cellController;
final ReminderBloc _reminderBloc;
void Function()? _onCellChangedFn;
void _dispatch() {
@@ -52,17 +52,15 @@ class DateCellEditorBloc
(event, emit) async {
await event.when(
didReceiveCellUpdate: (DateCellDataPB? cellData) {
final dateCellData = _dateDataFromCellData(cellData);
final endDay =
dateCellData.isRange == state.isRange && dateCellData.isRange
? dateCellData.endDateTime
: null;
final dateCellData = DateCellData.fromPB(cellData);
ReminderOption option = state.reminderOption;
final cellReminderId = dateCellData.reminderId;
if (dateCellData.dateTime != null &&
(state.reminderId?.isEmpty ?? true) &&
(dateCellData.reminderId?.isNotEmpty ?? false) &&
state.reminderOption != ReminderOption.none) {
state.reminderId.isEmpty &&
state.reminderOption != ReminderOption.none &&
cellReminderId.isNotEmpty) {
final date = state.reminderOption.withoutTime
? dateCellData.dateTime!.withoutTime
: dateCellData.dateTime!;
@@ -70,7 +68,7 @@ class DateCellEditorBloc
// Add Reminder
_reminderBloc.add(
ReminderEvent.addById(
reminderId: dateCellData.reminderId!,
reminderId: dateCellData.reminderId,
objectId: cellController.viewId,
meta: {
ReminderMetaKeys.includeTime: true.toString(),
@@ -78,7 +76,7 @@ class DateCellEditorBloc
},
scheduledAt: Int64(
state.reminderOption
.fromDate(date)
.getNotificationDateTime(date)
.millisecondsSinceEpoch ~/
1000,
),
@@ -86,8 +84,7 @@ class DateCellEditorBloc
);
}
if ((dateCellData.reminderId?.isNotEmpty ?? false) &&
dateCellData.dateTime != null) {
if (cellReminderId.isNotEmpty && dateCellData.dateTime != null) {
if (option.requiresNoTime && dateCellData.includeTime) {
option = ReminderOption.atTimeOfEvent;
} else if (!option.withoutTime && !dateCellData.includeTime) {
@@ -98,13 +95,13 @@ class DateCellEditorBloc
? dateCellData.dateTime!.withoutTime
: dateCellData.dateTime!;
final scheduledAt = option.fromDate(date);
final scheduledAt = option.getNotificationDateTime(date);
// Update Reminder
_reminderBloc.add(
ReminderEvent.update(
ReminderUpdate(
id: dateCellData.reminderId!,
id: dateCellData.reminderId,
scheduledAt: scheduledAt,
includeTime: true,
),
@@ -115,129 +112,75 @@ class DateCellEditorBloc
emit(
state.copyWith(
dateTime: dateCellData.dateTime,
timeStr: dateCellData.timeStr,
endDateTime: dateCellData.endDateTime,
endTimeStr: dateCellData.endTimeStr,
includeTime: dateCellData.includeTime,
isRange: dateCellData.isRange,
startDay: dateCellData.isRange ? dateCellData.dateTime : null,
endDay: endDay,
dateStr: dateCellData.dateStr,
endDateStr: dateCellData.endDateStr,
reminderId: dateCellData.reminderId,
reminderOption: option,
),
);
},
didReceiveTimeFormatError: (
String? parseTimeError,
String? parseEndTimeError,
) {
emit(
state.copyWith(
parseTimeError: parseTimeError,
parseEndTimeError: parseEndTimeError,
),
);
didUpdateField: (field) {
final typeOption = DateTypeOptionDataParser()
.fromBuffer(field.field.typeOptionData);
emit(state.copyWith(dateTypeOptionPB: typeOption));
},
selectDay: (date) async {
updateDateTime: (date) async {
if (state.isRange) {
return;
}
await _updateDateData(date: date);
},
updateDateRange: (DateTime start, DateTime end) async {
if (!state.isRange) {
await _updateDateData(date: date);
return;
}
await _updateDateData(date: start, endDate: end);
},
setIncludeTime: (includeTime) async =>
_updateDateData(includeTime: includeTime),
setIsRange: (isRange) async => _updateDateData(isRange: isRange),
setTime: (timeStr) async {
emit(state.copyWith(timeStr: timeStr));
await _updateDateData(timeStr: timeStr);
setIncludeTime: (includeTime, dateTime, endDateTime) async {
await _updateIncludeTime(includeTime, dateTime, endDateTime);
},
selectDateRange: (DateTime? start, DateTime? end) async {
if (end == null && state.startDay != null && state.endDay == null) {
final (newStart, newEnd) = state.startDay!.isBefore(start!)
? (state.startDay!, start)
: (start, state.startDay!);
emit(state.copyWith(startDay: null, endDay: null));
await _updateDateData(date: newStart.date, endDate: newEnd.date);
} else if (end == null) {
emit(state.copyWith(startDay: start, endDay: null));
} else {
await _updateDateData(date: start!.date, endDate: end.date);
}
setIsRange: (isRange, dateTime, endDateTime) async {
await _updateIsRange(isRange, dateTime, endDateTime);
},
setStartDay: (DateTime startDay) async {
if (state.endDay == null) {
emit(state.copyWith(startDay: startDay));
} else if (startDay.isAfter(state.endDay!)) {
emit(state.copyWith(startDay: startDay, endDay: null));
} else {
emit(state.copyWith(startDay: startDay));
await _updateDateData(
date: startDay.date,
endDate: state.endDay!.date,
);
}
setDateFormat: (DateFormatPB dateFormat) async {
await _updateTypeOption(emit, dateFormat: dateFormat);
},
setEndDay: (DateTime endDay) {
if (state.startDay == null) {
emit(state.copyWith(endDay: endDay));
} else if (endDay.isBefore(state.startDay!)) {
emit(state.copyWith(startDay: null, endDay: endDay));
} else {
emit(state.copyWith(endDay: endDay));
_updateDateData(date: state.startDay!.date, endDate: endDay.date);
}
setTimeFormat: (TimeFormatPB timeFormat) async {
await _updateTypeOption(emit, timeFormat: timeFormat);
},
setEndTime: (String? endTime) async {
emit(state.copyWith(endTimeStr: endTime));
await _updateDateData(endTimeStr: endTime);
},
setDateFormat: (DateFormatPB dateFormat) async =>
await _updateTypeOption(emit, dateFormat: dateFormat),
setTimeFormat: (TimeFormatPB timeFormat) async =>
await _updateTypeOption(emit, timeFormat: timeFormat),
clearDate: () async {
// Remove reminder if neccessary
if (state.reminderId != null) {
if (state.reminderId.isNotEmpty) {
_reminderBloc
.add(ReminderEvent.remove(reminderId: state.reminderId!));
.add(ReminderEvent.remove(reminderId: state.reminderId));
}
await _clearDate();
},
setReminderOption: (
ReminderOption option,
DateTime? selectedDay,
) async {
if (state.reminderId?.isEmpty ??
true &&
(state.dateTime != null || selectedDay != null) &&
option != ReminderOption.none) {
if (option == ReminderOption.none && state.reminderId.isNotEmpty) {
_reminderBloc.add(
ReminderEvent.remove(reminderId: state.reminderId),
);
await _updateDateData(reminderId: "");
} else if (state.reminderId.isEmpty) {
// New Reminder
final reminderId = nanoid();
await _updateDateData(reminderId: reminderId, date: selectedDay);
emit(
state.copyWith(reminderOption: option, dateTime: selectedDay),
await _updateDateData(
reminderId: reminderId,
date: state.dateTime ?? DateTime.now().withoutTime,
);
} else if (option == ReminderOption.none &&
(state.reminderId?.isNotEmpty ?? false)) {
// Remove reminder
_reminderBloc
.add(ReminderEvent.remove(reminderId: state.reminderId!));
await _updateDateData(reminderId: "");
emit(state.copyWith(reminderOption: option));
} else if (state.dateTime != null &&
(state.reminderId?.isNotEmpty ?? false)) {
final scheduledAt = option.fromDate(state.dateTime!);
} else if (state.dateTime != null) {
// Update reminder
final scheduledAt =
option.getNotificationDateTime(state.dateTime!);
_reminderBloc.add(
ReminderEvent.update(
ReminderUpdate(
id: state.reminderId!,
id: state.reminderId,
scheduledAt: scheduledAt,
includeTime: true,
),
@@ -245,8 +188,9 @@ class DateCellEditorBloc
);
}
},
// Empty String signifies no reminder
removeReminder: () async => _updateDateData(reminderId: ""),
removeReminder: () async {
await _updateDateData(reminderId: "");
},
);
},
);
@@ -254,102 +198,46 @@ class DateCellEditorBloc
Future<void> _updateDateData({
DateTime? date,
String? timeStr,
DateTime? endDate,
String? endTimeStr,
bool? includeTime,
bool? isRange,
String? reminderId,
}) async {
// make sure that not both date and time are updated at the same time
assert(
!(date != null && timeStr != null) ||
!(endDate != null && endTimeStr != null),
);
// if not updating the time, use the old time in the state
final String? newTime = timeStr ?? state.timeStr;
final DateTime? newDate = timeStr != null && timeStr.isNotEmpty
? state.dateTime ?? DateTime.now()
: _utcToLocalAndAddCurrentTime(date);
// if not updating the time, use the old time in the state
final String? newEndTime = endTimeStr ?? state.endTimeStr;
final DateTime? newEndDate = endTimeStr != null && endTimeStr.isNotEmpty
? state.endDateTime ?? DateTime.now()
: _utcToLocalAndAddCurrentTime(endDate);
final result = await _dateCellBackendService.update(
date: newDate,
time: newTime,
endDate: newEndDate,
endTime: newEndTime,
includeTime: includeTime ?? state.includeTime,
isRange: isRange ?? state.isRange,
reminderId: reminderId ?? state.reminderId,
date: date,
endDate: endDate,
reminderId: reminderId,
);
result.onFailure(Log.error);
}
result.fold(
(_) {
if (!isClosed &&
(state.parseEndTimeError != null || state.parseTimeError != null)) {
add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
}
},
(err) {
switch (err.code) {
case ErrorCode.InvalidDateTimeFormat:
if (isClosed) {
return;
}
// to determine which textfield should show error
final (startError, endError) = newDate != null
? (timeFormatPrompt(err), null)
: (null, timeFormatPrompt(err));
add(
DateCellEditorEvent.didReceiveTimeFormatError(
startError,
endError,
),
);
break;
default:
Log.error(err);
}
},
Future<void> _updateIsRange(
bool isRange,
DateTime? dateTime,
DateTime? endDateTime,
) async {
final result = await _dateCellBackendService.update(
date: dateTime,
endDate: endDateTime,
isRange: isRange,
);
result.onFailure(Log.error);
}
Future<void> _updateIncludeTime(
bool includeTime,
DateTime? dateTime,
DateTime? endDateTime,
) async {
final result = await _dateCellBackendService.update(
date: dateTime,
endDate: endDateTime,
includeTime: includeTime,
);
result.onFailure(Log.error);
}
Future<void> _clearDate() async {
final result = await _dateCellBackendService.clear();
result.fold(
(_) {
if (!isClosed) {
add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null));
}
},
(err) => Log.error(err),
);
}
DateTime? _utcToLocalAndAddCurrentTime(DateTime? date) {
if (date == null) {
return null;
}
final now = DateTime.now();
// the incoming date is Utc. This trick converts it into Local
// and add the current time. The time may be overwritten by
// explicitly provided time string in the backend though
return DateTime(
date.year,
date.month,
date.day,
now.hour,
now.minute,
now.second,
);
result.onFailure(Log.error);
}
String timeFormatPrompt(FlowyError error) {
@@ -367,6 +255,7 @@ class DateCellEditorBloc
if (_onCellChangedFn != null) {
cellController.removeListener(
onCellChanged: _onCellChangedFn!,
onFieldChanged: _onFieldChangedListener,
);
}
return super.close();
@@ -379,10 +268,17 @@ class DateCellEditorBloc
add(DateCellEditorEvent.didReceiveCellUpdate(cell));
}
},
onFieldChanged: _onFieldChangedListener,
);
}
Future<void>? _updateTypeOption(
void _onFieldChangedListener(FieldInfo fieldInfo) {
if (!isClosed) {
add(DateCellEditorEvent.didUpdateField(fieldInfo));
}
}
Future<void> _updateTypeOption(
Emitter<DateCellEditorState> emit, {
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
@@ -404,59 +300,43 @@ class DateCellEditorBloc
typeOptionData: newDateTypeOption.writeToBuffer(),
);
result.fold(
(_) => emit(
state.copyWith(
dateTypeOptionPB: newDateTypeOption,
timeHintText: _timeHintText(newDateTypeOption),
),
),
(err) => Log.error(err),
);
result.fold((_) => {}, (err) => Log.error(err));
}
}
@freezed
class DateCellEditorEvent with _$DateCellEditorEvent {
const factory DateCellEditorEvent.didUpdateField(
FieldInfo fieldInfo,
) = _DidUpdateField;
// notification that cell is updated in the backend
const factory DateCellEditorEvent.didReceiveCellUpdate(
DateCellDataPB? data,
) = _DidReceiveCellUpdate;
const factory DateCellEditorEvent.didReceiveTimeFormatError(
String? parseTimeError,
String? parseEndTimeError,
) = _DidReceiveTimeFormatError;
const factory DateCellEditorEvent.updateDateTime(DateTime day) =
_UpdateDateTime;
// date cell data is modified
const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellEditorEvent.updateDateRange(
DateTime start,
DateTime end,
) = _UpdateDateRange;
const factory DateCellEditorEvent.selectDateRange(
DateTime? start,
DateTime? end,
) = _SelectDateRange;
const factory DateCellEditorEvent.setIncludeTime(
bool includeTime,
DateTime? dateTime,
DateTime? endDateTime,
) = _IncludeTime;
const factory DateCellEditorEvent.setStartDay(
DateTime startDay,
) = _SetStartDay;
const factory DateCellEditorEvent.setIsRange(
bool isRange,
DateTime? dateTime,
DateTime? endDateTime,
) = _SetIsRange;
const factory DateCellEditorEvent.setEndDay(
DateTime endDay,
) = _SetEndDay;
const factory DateCellEditorEvent.setTime(String time) = _SetTime;
const factory DateCellEditorEvent.setEndTime(String endTime) = _SetEndTime;
const factory DateCellEditorEvent.setIncludeTime(bool includeTime) =
_IncludeTime;
const factory DateCellEditorEvent.setIsRange(bool isRange) = _SetIsRange;
const factory DateCellEditorEvent.setReminderOption({
required ReminderOption option,
@Default(null) DateTime? selectedDay,
}) = _SetReminderOption;
const factory DateCellEditorEvent.setReminderOption(ReminderOption option) =
_SetReminderOption;
const factory DateCellEditorEvent.removeReminder() = _RemoveReminder;
@@ -476,25 +356,12 @@ class DateCellEditorState with _$DateCellEditorState {
// the date field's type option
required DateTypeOptionPB dateTypeOptionPB,
// used when selecting a date range
required DateTime? startDay,
required DateTime? endDay,
// cell data from the backend
required DateTime? dateTime,
required DateTime? endDateTime,
required String? timeStr,
required String? endTimeStr,
required bool includeTime,
required bool isRange,
required String? dateStr,
required String? endDateStr,
required String? reminderId,
// error and hint text
required String? parseTimeError,
required String? parseEndTimeError,
required String timeHintText,
required String reminderId,
@Default(ReminderOption.none) ReminderOption reminderOption,
}) = _DateCellEditorState;
@@ -504,11 +371,11 @@ class DateCellEditorState with _$DateCellEditorState {
) {
final typeOption = controller.getTypeOption(DateTypeOptionDataParser());
final cellData = controller.getCellData();
final dateCellData = _dateDataFromCellData(cellData);
final dateCellData = DateCellData.fromPB(cellData);
ReminderOption reminderOption = ReminderOption.none;
if ((dateCellData.reminderId?.isNotEmpty ?? false) &&
dateCellData.dateTime != null) {
if (dateCellData.reminderId.isNotEmpty && dateCellData.dateTime != null) {
final reminder = reminderBloc.state.reminders
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
if (reminder != null) {
@@ -524,114 +391,60 @@ class DateCellEditorState with _$DateCellEditorState {
return DateCellEditorState(
dateTypeOptionPB: typeOption,
startDay: dateCellData.isRange ? dateCellData.dateTime : null,
endDay: dateCellData.isRange ? dateCellData.endDateTime : null,
dateTime: dateCellData.dateTime,
endDateTime: dateCellData.endDateTime,
timeStr: dateCellData.timeStr,
endTimeStr: dateCellData.endTimeStr,
dateStr: dateCellData.dateStr,
endDateStr: dateCellData.endDateStr,
includeTime: dateCellData.includeTime,
isRange: dateCellData.isRange,
parseTimeError: null,
parseEndTimeError: null,
timeHintText: _timeHintText(typeOption),
reminderId: dateCellData.reminderId,
reminderOption: reminderOption,
);
}
}
String _timeHintText(DateTypeOptionPB typeOption) {
switch (typeOption.timeFormat) {
case TimeFormatPB.TwelveHour:
return LocaleKeys.document_date_timeHintTextInTwelveHour.tr();
case TimeFormatPB.TwentyFourHour:
return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr();
default:
return "";
}
}
_DateCellData _dateDataFromCellData(
DateCellDataPB? cellData,
) {
// a null DateCellDataPB may be returned, indicating that all the fields are
// their default values: empty strings and false booleans
if (cellData == null) {
return _DateCellData(
dateTime: null,
endDateTime: null,
timeStr: null,
endTimeStr: null,
includeTime: false,
isRange: false,
dateStr: null,
endDateStr: null,
reminderId: null,
);
}
DateTime? dateTime;
String? timeStr;
DateTime? endDateTime;
String? endTimeStr;
String? endDateStr;
if (cellData.hasTimestamp()) {
final timestamp = cellData.timestamp * 1000;
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
timeStr = cellData.time;
if (cellData.hasEndTimestamp()) {
final endTimestamp = cellData.endTimestamp * 1000;
endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt());
endTimeStr = cellData.endTime;
}
}
final bool includeTime = cellData.includeTime;
final bool isRange = cellData.isRange;
if (cellData.isRange) {
endDateStr = cellData.endDate;
}
final String dateStr = cellData.date;
return _DateCellData(
dateTime: dateTime,
endDateTime: endDateTime,
timeStr: timeStr,
endTimeStr: endTimeStr,
includeTime: includeTime,
isRange: isRange,
dateStr: dateStr,
endDateStr: endDateStr,
reminderId: cellData.reminderId,
);
}
class _DateCellData {
_DateCellData({
/// Helper class to parse ProtoBuf payloads into DateCellEditorState
class DateCellData {
const DateCellData({
required this.dateTime,
required this.endDateTime,
required this.timeStr,
required this.endTimeStr,
required this.includeTime,
required this.isRange,
required this.dateStr,
required this.endDateStr,
required this.reminderId,
});
const DateCellData.empty()
: dateTime = null,
endDateTime = null,
includeTime = false,
isRange = false,
reminderId = "";
factory DateCellData.fromPB(DateCellDataPB? cellData) {
// a null DateCellDataPB may be returned, indicating that all the fields are
// their default values: empty strings and false booleans
if (cellData == null) {
return const DateCellData.empty();
}
final dateTime =
cellData.hasTimestamp() ? cellData.timestamp.toDateTime() : null;
final endDateTime = dateTime == null || !cellData.isRange
? null
: cellData.hasEndTimestamp()
? cellData.endTimestamp.toDateTime()
: null;
return DateCellData(
dateTime: dateTime,
endDateTime: endDateTime,
includeTime: cellData.includeTime,
isRange: cellData.isRange,
reminderId: cellData.reminderId,
);
}
final DateTime? dateTime;
final DateTime? endDateTime;
final String? timeStr;
final String? endTimeStr;
final bool includeTime;
final bool isRange;
final String? dateStr;
final String? endDateStr;
final String? reminderId;
final String reminderId;
}
@@ -3,12 +3,14 @@ import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -24,7 +26,10 @@ class MediaCellBloc extends Bloc<MediaCellEvent, MediaCellState> {
_startListening();
}
late final RowBackendService _rowService =
RowBackendService(viewId: cellController.viewId);
final MediaCellController cellController;
void Function()? _onCellChangedFn;
String get databaseId => cellController.viewId;
@@ -110,10 +115,6 @@ class MediaCellBloc extends Bloc<MediaCellEvent, MediaCellState> {
},
reorderFiles: (from, to) async {
final files = List<MediaFilePB>.from(state.files);
if (from < to) {
to--;
}
files.insert(to, files.removeAt(from));
// We emit the new state first to update the UI
@@ -153,6 +154,10 @@ class MediaCellBloc extends Bloc<MediaCellEvent, MediaCellState> {
toggleShowAllFiles: () {
emit(state.copyWith(showAllFiles: !state.showAllFiles));
},
setCover: (cover) => _rowService.updateMeta(
rowId: cellController.rowId,
cover: cover,
),
);
},
);
@@ -214,6 +219,8 @@ class MediaCellEvent with _$MediaCellEvent {
}) = _RenameFile;
const factory MediaCellEvent.toggleShowAllFiles() = _ToggleShowAllFiles;
const factory MediaCellEvent.setCover(RowCoverPB cover) = _SetCover;
}
@freezed
@@ -223,7 +230,7 @@ class MediaCellState with _$MediaCellState {
required String fieldName,
@Default([]) List<MediaFilePB> files,
@Default(false) showAllFiles,
@Default(false) hideFileNames,
@Default(true) hideFileNames,
}) = _MediaCellState;
factory MediaCellState.initial(MediaCellController cellController) {
@@ -241,15 +241,11 @@ class CellDataNotifier<T> extends ChangeNotifier {
bool Function(T? oldValue, T? newValue)? listenWhen;
set value(T newValue) {
if (listenWhen?.call(_value, newValue) ?? false) {
_value = newValue;
notifyListeners();
} else {
if (_value != newValue) {
_value = newValue;
notifyListeners();
}
if (listenWhen != null && !listenWhen!.call(_value, newValue)) {
return;
}
_value = newValue;
notifyListeners();
}
T get value => _value;
@@ -132,6 +132,24 @@ class DatabaseController {
}
}
void removeListener({
DatabaseCallbacks? onDatabaseChanged,
DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged,
GroupCallbacks? onGroupChanged,
}) {
if (onDatabaseChanged != null) {
_databaseCallbacks.remove(onDatabaseChanged);
}
if (onLayoutSettingsChanged != null) {
_layoutCallbacks.remove(onLayoutSettingsChanged);
}
if (onGroupChanged != null) {
_groupCallbacks.remove(onGroupChanged);
}
}
Future<FlowyResult<void, FlowyError>> open() async {
return _databaseViewBackendSvc.openDatabase().then((result) {
return result.fold(
@@ -40,6 +40,7 @@ class BoardActionsCubit extends Cubit<BoardActionsState> {
}
void startCreateBottomRow(String groupId) {
emit(const BoardActionsState.setFocus(groupedRowIds: []));
emit(BoardActionsState.startCreateBottomRow(groupId: groupId));
emit(const BoardActionsState.initial());
}
@@ -55,6 +55,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId;
DatabaseCallbacks? _databaseCallbacks;
DatabaseLayoutSettingCallbacks? _layoutSettingsCallback;
GroupCallbacks? _groupCallbacks;
void _initBoardController(AppFlowyBoardController? controller) {
boardController = controller ??
AppFlowyBoardController(
@@ -146,18 +150,18 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
},
createGroup: (name) async {
final result = await groupBackendSvc.createGroup(name: name);
result.fold((_) {}, (err) => Log.error(err));
result.onFailure(Log.error);
},
deleteGroup: (groupId) async {
final result = await groupBackendSvc.deleteGroup(groupId: groupId);
result.fold((_) {}, (err) => Log.error(err));
result.onFailure(Log.error);
},
renameGroup: (groupId, name) async {
final result = await groupBackendSvc.updateGroup(
groupId: groupId,
name: name,
);
result.fold((_) {}, (err) => Log.error(err));
result.onFailure(Log.error);
},
didReceiveError: (error) {
emit(BoardState.error(error: error));
@@ -330,6 +334,17 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
for (final controller in groupControllers.values) {
await controller.dispose();
}
databaseController.removeListener(
onDatabaseChanged: _databaseCallbacks,
onLayoutSettingsChanged: _layoutSettingsCallback,
onGroupChanged: _groupCallbacks,
);
_databaseCallbacks = null;
_layoutSettingsCallback = null;
_groupCallbacks = null;
boardController.dispose();
return super.close();
}
@@ -380,7 +395,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
RowCache get rowCache => databaseController.rowCache;
void _startListening() {
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
_layoutSettingsCallback = DatabaseLayoutSettingCallbacks(
onLayoutSettingsChanged: (layoutSettings) {
if (isClosed) {
return;
@@ -403,7 +418,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board));
},
);
final onGroupChanged = GroupCallbacks(
_groupCallbacks = GroupCallbacks(
onGroupByField: (groups) {
if (isClosed) {
return;
@@ -482,7 +497,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
add(BoardEvent.didReceiveGroups(groupList));
},
);
final onDatabaseChanged = DatabaseCallbacks(
_databaseCallbacks = DatabaseCallbacks(
onRowsCreated: (rows) {
for (final row in rows) {
if (!isClosed && row.isHiddenInView) {
@@ -493,9 +508,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutSettingsChanged: onLayoutSettingsChanged,
onGroupChanged: onGroupChanged,
onDatabaseChanged: _databaseCallbacks,
onLayoutSettingsChanged: _layoutSettingsCallback,
onGroupChanged: _groupCallbacks,
);
}
@@ -534,7 +534,9 @@ class _BoardColumnFooterState extends State<BoardColumnFooter> {
color: Theme.of(context).hintColor,
),
onTap: () {
setState(() => _isCreating = true);
context
.read<BoardActionsCubit>()
.startCreateBottomRow(widget.columnData.id);
},
),
),
@@ -8,7 +8,6 @@ import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -51,17 +51,7 @@ class _EditableColumnHeaderState extends State<EditableColumnHeader> {
}
return KeyEventResult.ignored;
},
)..addListener(() {
if (!focusNode.hasFocus) {
widget.isEditing.value = false;
widget.onSubmitted(textController.text);
} else {
textController.selection = TextSelection(
baseOffset: 0,
extentOffset: textController.text.length,
);
}
});
)..addListener(onFocusChanged);
}
@override
@@ -74,11 +64,25 @@ class _EditableColumnHeaderState extends State<EditableColumnHeader> {
@override
void dispose() {
focusNode.dispose();
focusNode
..removeListener(onFocusChanged)
..dispose();
textController.dispose();
super.dispose();
}
void onFocusChanged() {
if (!focusNode.hasFocus) {
widget.isEditing.value = false;
widget.onSubmitted(textController.text);
} else {
textController.selection = TextSelection(
baseOffset: 0,
extentOffset: textController.text.length,
);
}
}
@override
Widget build(BuildContext context) {
return Row(
@@ -16,7 +16,6 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -37,6 +37,20 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
DatabaseCallbacks? _databaseCallbacks;
DatabaseLayoutSettingCallbacks? _layoutSettingCallbacks;
@override
Future<void> close() async {
databaseController.removeListener(
onDatabaseChanged: _databaseCallbacks,
onLayoutSettingsChanged: _layoutSettingCallbacks,
);
_databaseCallbacks = null;
_layoutSettingCallbacks = null;
await super.close();
}
void _dispatch() {
on<CalendarEvent>(
(event, emit) async {
@@ -300,7 +314,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
}
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
_databaseCallbacks = DatabaseCallbacks(
onDatabaseChanged: (database) {
if (isClosed) return;
},
@@ -375,13 +389,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
},
);
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
_layoutSettingCallbacks = DatabaseLayoutSettingCallbacks(
onLayoutSettingsChanged: _didReceiveLayoutSetting,
);
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutSettingsChanged: onLayoutSettingsChanged,
onDatabaseChanged: _databaseCallbacks,
onLayoutSettingsChanged: _layoutSettingCallbacks,
);
}
@@ -29,6 +29,15 @@ class UnscheduleEventsBloc
CellMemCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
DatabaseCallbacks? _databaseCallbacks;
@override
Future<void> close() async {
databaseController.removeListener(onDatabaseChanged: _databaseCallbacks);
_databaseCallbacks = null;
await super.close();
}
void _dispatch() {
on<UnscheduleEventsEvent>(
(event, emit) async {
@@ -103,7 +112,7 @@ class UnscheduleEventsBloc
}
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
_databaseCallbacks = DatabaseCallbacks(
onRowsCreated: (rows) async {
if (isClosed) {
return;
@@ -135,7 +144,7 @@ class UnscheduleEventsBloc
},
);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
databaseController.addListener(onDatabaseChanged: _databaseCallbacks);
}
}
@@ -242,6 +242,7 @@ class NewEventButton extends StatelessWidget {
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 22,
tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(),
radius: Corners.s6Border,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
@@ -251,7 +252,7 @@ class NewEventButton extends StatelessWidget {
width: 0.5,
),
),
borderRadius: Corners.s5Border,
borderRadius: Corners.s6Border,
boxShadow: [
BoxShadow(
spreadRadius: -2,
@@ -9,7 +9,6 @@ import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -13,7 +13,6 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/te
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -15,7 +15,6 @@ 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-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@@ -437,7 +436,6 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
),
side: BorderSide(color: Theme.of(context).dividerColor),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
visualDensity: VisualDensity.compact,
),
onPressed: () {
if (state.unscheduleEvents.isNotEmpty) {
@@ -8,7 +8,6 @@ import 'package:appflowy/plugins/database/calendar/application/calendar_setting_
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -10,7 +10,7 @@ final class DateCellBackendService {
required String viewId,
required String fieldId,
required String rowId,
}) : cellId = CellIdPB.create()
}) : cellId = CellIdPB()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
@@ -18,32 +18,27 @@ final class DateCellBackendService {
final CellIdPB cellId;
Future<FlowyResult<void, FlowyError>> update({
required bool includeTime,
required bool isRange,
bool? includeTime,
bool? isRange,
DateTime? date,
String? time,
DateTime? endDate,
String? endTime,
String? reminderId,
}) {
final payload = DateCellChangesetPB.create()
..cellId = cellId
..includeTime = includeTime
..isRange = isRange;
final payload = DateCellChangesetPB()..cellId = cellId;
if (includeTime != null) {
payload.includeTime = includeTime;
}
if (isRange != null) {
payload.isRange = isRange;
}
if (date != null) {
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
payload.date = Int64(dateTimestamp);
}
if (time != null) {
payload.time = time;
payload.timestamp = Int64(dateTimestamp);
}
if (endDate != null) {
final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000;
payload.endDate = Int64(dateTimestamp);
}
if (endTime != null) {
payload.endTime = endTime;
payload.endTimestamp = Int64(dateTimestamp);
}
if (reminderId != null) {
payload.reminderId = reminderId;
@@ -53,7 +48,7 @@ final class DateCellBackendService {
}
Future<FlowyResult<void, FlowyError>> clear() {
final payload = DateCellChangesetPB.create()
final payload = DateCellChangesetPB()
..cellId = cellId
..clearFlag = true;
@@ -80,12 +80,8 @@ class FilterEditorBloc extends Bloc<FilterEditorEvent, FilterEditorState> {
};
fieldController.addListener(
onFilters: (filters) {
_onFilterFn?.call(filters);
},
onReceiveFields: (fields) {
_onFieldFn?.call(fields);
},
onFilters: _onFilterFn,
onReceiveFields: _onFieldFn,
);
}
@@ -32,6 +32,15 @@ class GridBloc extends Bloc<GridEvent, GridState> {
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
DatabaseCallbacks? _databaseCallbacks;
@override
Future<void> close() async {
databaseController.removeListener(onDatabaseChanged: _databaseCallbacks);
_databaseCallbacks = null;
await super.close();
}
void _dispatch() {
on<GridEvent>(
(event, emit) async {
@@ -120,7 +129,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
RowCache get rowCache => databaseController.rowCache;
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
_databaseCallbacks = DatabaseCallbacks(
onNumOfRowsChanged: (rowInfos, _, reason) {
if (!isClosed) {
add(GridEvent.didLoadRows(rowInfos, reason));
@@ -157,7 +166,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
}
},
);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
databaseController.addListener(onDatabaseChanged: _databaseCallbacks);
}
Future<void> _openGrid(Emitter<GridState> emit) async {
@@ -20,6 +20,12 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
final String viewId;
final FieldController fieldController;
@override
Future<void> close() async {
fieldController.removeListener(onFieldsListener: _onReceiveFields);
await super.close();
}
void _dispatch() {
on<GridHeaderEvent>(
(event, emit) async {
@@ -82,11 +88,13 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
void _startListening() {
fieldController.addListener(
onReceiveFields: (fields) =>
add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
onReceiveFields: _onReceiveFields,
listenWhen: () => !isClosed,
);
}
void _onReceiveFields(List<FieldInfo> fields) =>
add(GridHeaderEvent.didReceiveFieldUpdate(fields));
}
@freezed
@@ -24,6 +24,15 @@ class MobileRowDetailBloc
UserProfilePB? _userProfile;
UserProfilePB? get userProfile => _userProfile;
DatabaseCallbacks? _databaseCallbacks;
@override
Future<void> close() async {
databaseController.removeListener(onDatabaseChanged: _databaseCallbacks);
_databaseCallbacks = null;
await super.close();
}
void _dispatch() {
on<MobileRowDetailEvent>(
(event, emit) {
@@ -67,7 +76,7 @@ class MobileRowDetailBloc
}
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
_databaseCallbacks = DatabaseCallbacks(
onNumOfRowsChanged: (rowInfos, _, reason) {
if (!isClosed) {
add(MobileRowDetailEvent.didLoadRows(rowInfos));
@@ -83,7 +92,7 @@ class MobileRowDetailBloc
}
},
);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
databaseController.addListener(onDatabaseChanged: _databaseCallbacks);
}
}
@@ -70,14 +70,13 @@ class RowBloc extends Bloc<RowEvent, RowState> {
);
}
void _startListening() {
_rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(RowEvent.didReceiveCells(cells, reason));
}
},
);
void _startListening() =>
_rowController.addListener(onRowChanged: _onRowChanged);
void _onRowChanged(List<CellContext> cells, ChangedReason reason) {
if (!isClosed) {
add(RowEvent.didReceiveCells(cells, reason));
}
}
void _init() {
@@ -10,7 +10,6 @@ import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -4,7 +4,6 @@ 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/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -3,7 +3,6 @@ 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/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -6,8 +6,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -199,17 +198,14 @@ class _DateFilterEditorState extends State<DateFilterEditor> {
child: SingleFilterBlocSelector<DateTimeFilter>(
filterId: widget.filterId,
builder: (context, filter, field) {
return AppFlowyDatePicker(
return DesktopAppFlowyDatePicker(
isRange: isRange,
timeHintText: LocaleKeys.grid_field_selectTime.tr(),
includeTime: false,
dateFormat: DateFormatPB.Friendly,
timeFormat: TimeFormatPB.TwentyFourHour,
selectedDay: isRange ? filter.start : filter.timestamp,
startDay: isRange ? filter.start : null,
endDay: isRange ? filter.end : null,
enableReminder: false,
onDaySelected: (selectedDay, _) {
dateTime: isRange ? filter.start : filter.timestamp,
endDateTime: isRange ? filter.end : null,
onDaySelected: (selectedDay) {
final newFilter = isRange
? filter.copyWithRange(start: selectedDay, end: null)
: filter.copyWithTimestamp(timestamp: selectedDay);
@@ -220,7 +216,7 @@ class _DateFilterEditorState extends State<DateFilterEditor> {
popooverController.close();
}
},
onRangeSelected: (start, end, _) {
onRangeSelected: (start, end) {
final newFilter = filter.copyWithRange(
start: start,
end: end,
@@ -5,7 +5,6 @@ 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/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -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_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -5,7 +5,6 @@ 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/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -5,7 +5,6 @@ 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/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -1,7 +1,6 @@
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/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Loaded 100 of 630 files, more files were not shown because too many files have changed in this diff. Show more