diff --git a/CHANGELOG.md b/CHANGELOG.md index 392f307d7e..25eef12f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Release Notes +## Version 0.1.1 - 03/21/2023 + +### New features + +- AppFlowy brings the power of OpenAI into your AppFlowy pages. Ask AI to write anything for you in AppFlowy. +- Support adding a cover image to your page, making your pages beautiful. +- More shortcuts become available. Click on '?' at the bottom right to access our shortcut guide. + +### Bug Fixes + +- Fix some bugs + ## Version 0.1.0 - 02/09/2023 ### New features diff --git a/README.md b/README.md index 93d6a11c70..5fa94d1b30 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,16 @@ You are in charge of your data and customizations.

AppFlowy Github - how to star the repo

## Getting Started with development + Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) for OS specific development instructions ## Roadmap -- [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap) -- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) - +* [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap) +* [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+)
-If you'd like to report a bug, submit bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+) +If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+) ## **Releases** @@ -61,37 +61,37 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab ## Contributing -Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details. +Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details. -If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, congratulations! If your administrative and managerial work behind the scenes that sustains the community as a whole, congratulations! You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt! +If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt! Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter. +## Join the community to build AppFlowy together -## Join the community to build AppFlowy together! ## Why Are We Building This? -Notion has been our favorite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints. +Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints. -The limitations we encountered using these tools rooted in our past work experience with collaborative productivity tools lead to our firm belief that there is, and will be a glass ceiling on what's possible in the future for tools like Notion. This emanates from these tools probable struggles to scale horizontally at some point. It implies that they will likely be forced to prioritize for a proportion of customers whose needs can be quite different from the rest. While decision-makers want a workplace OS, the truth is that it is not very possible to come up with a one-size fits all solution in such a fragmented market. +The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market. When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well. All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well. -- To individuals, we would like to offer Notion's functionality along with data security and cross-platform native experience. -- To enterprises and hackers, AppFlowy is dedicated to offering building blocks, that is, collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability. +* To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience. +* To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability. We decided to achieve this mission by upholding the three most fundamental values: -- Data privacy first -- Reliable native experience -- Community-driven extensibility +* Data privacy first +* Reliable native experience +* Community-driven extensibility -To be honest, we do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools, while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks. +We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks. ## License @@ -101,6 +101,6 @@ Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppF Special thanks to these amazing projects which help power AppFlowy.IO: -- [flutter-quill](https://github.com/singerdmx/flutter-quill) -- [cargo-make](https://github.com/sagiegurari/cargo-make) -- [contrib.rocks](https://contrib.rocks) +* [flutter-quill](https://github.com/singerdmx/flutter-quill) +* [cargo-make](https://github.com/sagiegurari/cargo-make) +* [contrib.rocks](https://contrib.rocks) diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 657f42fdd9..0501207968 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -12,8 +12,8 @@ "type": "dart", "preLaunchTask": "AF: Build Appflowy Core", "env": { - // "RUST_LOG": "trace", - "RUST_LOG": "debug" + "RUST_LOG": "trace", + // "RUST_LOG": "debug" }, "cwd": "${workspaceRoot}/appflowy_flutter" }, diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 8ca63bfa72..1fc240a42a 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -CURRENT_APP_VERSION = "0.1.0" +CURRENT_APP_VERSION = "0.1.1" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html diff --git a/frontend/appflowy_flutter/.metadata b/frontend/appflowy_flutter/.metadata index a8ebbd603e..9068867840 100644 --- a/frontend/appflowy_flutter/.metadata +++ b/frontend/appflowy_flutter/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: fa5883b78e566877613ad1ccb48dd92075cb5c23 - channel: dev + revision: 135454af32477f815a7525073027a3ff9eff1bfd + channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: windows + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle index 216ec0c1fb..948e9b7f42 100644 --- a/frontend/appflowy_flutter/android/app/build.gradle +++ b/frontend/appflowy_flutter/android/app/build.gradle @@ -45,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.appflowy_flutter" + applicationId "io.appflowy.appflowy" minSdkVersion 19 targetSdkVersion 31 versionCode flutterVersionCode.toInteger() diff --git a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml index 49c97148ad..7d5632662e 100644 --- a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.appflowy.appflowy"> diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index 96e75259ad..264e1d3232 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.appflowy.appflowy"> + package="io.appflowy.appflowy"> diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index f19519d68c..bcca185f4e 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -138,7 +138,8 @@ "keep": "Keep", "tryAgain": "Try again", "discard": "Discard", - "replace": "Replace" + "replace": "Replace", + "insertBelow": "Insert Below" }, "label": { "welcome": "Welcome!", @@ -197,7 +198,8 @@ "browser": "Browse", "create": "Create", "folderPath": "Path to store your folder", - "locationCannotBeEmpty": "Path cannot be empty" + "locationCannotBeEmpty": "Path cannot be empty", + "pathCopiedSnackbar": "File storage path copied to clipboard!" }, "user": { "name": "Name", @@ -216,7 +218,8 @@ "addFilter": "Add Filter", "deleteFilter": "Delete filter", "filterBy": "Filter by...", - "typeAValue": "Type a value..." + "typeAValue": "Type a value...", + "layout": "Layout" }, "textFilter": { "contains": "Contains", @@ -333,29 +336,32 @@ }, "slashMenu": { "board": { - "selectABoardToLinkTo": "Select a Board to link to" + "selectABoardToLinkTo": "Select a Board to link to", + "createANewBoard": "Create a new Board" }, "grid": { - "selectAGridToLinkTo": "Select a Grid to link to" + "selectAGridToLinkTo": "Select a Grid to link to", + "createANewGrid": "Create a new Grid" } }, "plugins": { "referencedBoard": "Referenced Board", "referencedGrid": "Referenced Grid", - "autoCompletionMenuItemName": "Auto Completion", - "autoGeneratorMenuItemName": "Auto Generator", + "autoGeneratorMenuItemName": "OpenAI Writer", "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...", "autoGeneratorLearnMore": "Learn more", "autoGeneratorGenerate": "Generate", - "autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...", + "autoGeneratorHintText": "Ask OpenAI ...", "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key", - "smartEdit": "Smart Edit", - "smartEditTitleName": "OpenAI: Smart Edit", + "smartEdit": "AI Assistants", + "openAI": "OpenAI", "smartEditFixSpelling": "Fix spelling", + "warning": "⚠️ AI responses can be inaccurate or misleading.", "smartEditSummarize": "Summarize", "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", "smartEditCouldNotFetchKey": "Could not fetch OpenAI key", "smartEditDisabled": "Connect OpenAI in Settings", + "discardResponse": "Do you want to discard the AI responses?", "cover": { "changeCover": "Change Cover", "colors": "Colors", @@ -377,6 +383,7 @@ "imageSavingFailed": "Image Saving Failed", "addIcon": "Add Icon" } + } }, "board": { @@ -393,6 +400,12 @@ "jumpToday": "Jump to Today", "previousMonth": "Previous Month", "nextMonth": "Next Month" + }, + "settings": { + "showWeekNumbers": "Show week numbers", + "showWeekends": "Show weekends", + "firstDayOfWeek": "First day of week", + "layoutDateField": "Layout calendar by" } } -} \ No newline at end of file +} diff --git a/frontend/appflowy_flutter/assets/translations/pt-BR.json b/frontend/appflowy_flutter/assets/translations/pt-BR.json index aebe816ff0..512de346d4 100644 --- a/frontend/appflowy_flutter/assets/translations/pt-BR.json +++ b/frontend/appflowy_flutter/assets/translations/pt-BR.json @@ -349,7 +349,6 @@ "autoGeneratorGenerate": "Gerar", "autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...", "autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI", - "smartEditTitleName": "IA: edição inteligente", "smartEditFixSpelling": "Corrigir ortografia", "smartEditSummarize": "Resumir", "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI", diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart index ad372e32a8..555a5e41b1 100644 --- a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart +++ b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart @@ -1,5 +1,10 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart'; import 'package:appflowy/user/presentation/folder/folder_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -86,7 +91,7 @@ void main() { await tester.tapGoButton(); await tester.expectToSeeWelcomePage(); - // swith to user B + // switch to user B { await tester.openSettings(); await tester.openSettingsPage(SettingsPage.user); @@ -120,7 +125,7 @@ void main() { expect(find.textContaining(userA), findsOneWidget); } - // swith to the userB again + // switch to the userB again { await tester.openSettings(); await tester.openSettingsPage(SettingsPage.files); @@ -157,5 +162,135 @@ void main() { await TestFolder.currentLocation(), ); }); + + testWidgets('/board shortcut creates a new board', (tester) async { + const folderName = 'appflowy'; + await TestFolder.cleanTestLocation(folderName); + await TestFolder.setTestLocation(folderName); + + await tester.initializeAppFlowy(); + + // tap open button + await mockGetDirectoryPath(folderName); + await tester.tapOpenFolderButton(); + + await tester.wait(1000); + await tester.expectToSeeWelcomePage(); + + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Necessary for being able to enterText when not in debug mode + binding.testTextInput.register(); + + // Needs tab to obtain focus for the app flowy editor. + // by default the tap appears at the center of the widget. + final Finder editor = find.byType(AppFlowyEditor); + await tester.tap(editor); + await tester.pumpAndSettle(); + + // tester.sendText() cannot be used since the editor + // does not contain any EditableText widgets. + // to interact with the app during an integration test, + // simulate physical keyboard events. + await simulateKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.slash); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyB); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyO); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyA); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyR); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyD); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.pumpAndSettle(); + + // Checks whether the options in the selection menu + // for /board exist. + expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2)); + + // Finalizes the slash command that creates the board. + await simulateKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + + // Checks whether new board is referenced and properly on the page. + expect(find.byType(BuiltInPageWidget), findsOneWidget); + + // Checks whether the new board is in the side bar. + final sidebarLabel = LocaleKeys.newPageText.tr(); + expect(find.text(sidebarLabel), findsOneWidget); + }); + + testWidgets('/grid shortcut creates a new grid', (tester) async { + const folderName = 'appflowy'; + await TestFolder.cleanTestLocation(folderName); + await TestFolder.setTestLocation(folderName); + + await tester.initializeAppFlowy(); + + // tap open button + await mockGetDirectoryPath(folderName); + await tester.tapOpenFolderButton(); + + await tester.wait(1000); + await tester.expectToSeeWelcomePage(); + + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Necessary for being able to enterText when not in debug mode + binding.testTextInput.register(); + + // Needs tab to obtain focus for the app flowy editor. + // by default the tap appears at the center of the widget. + final Finder editor = find.byType(AppFlowyEditor); + await tester.tap(editor); + await tester.pumpAndSettle(); + + // tester.sendText() cannot be used since the editor + // does not contain any EditableText widgets. + // to interact with the app during an integration test, + // simulate physical keyboard events. + await simulateKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.slash); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyG); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyR); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyI); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.keyD); + await tester.pumpAndSettle(); + await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown); + await tester.pumpAndSettle(); + + // Checks whether the options in the selection menu + // for /grid exist. + expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2)); + + // Finalizes the slash command that creates the board. + await simulateKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + + // Checks whether new board is referenced and properly on the page. + expect(find.byType(BuiltInPageWidget), findsOneWidget); + + // Checks whether the new board is in the side bar. + final sidebarLabel = LocaleKeys.newPageText.tr(); + expect(find.text(sidebarLabel), findsOneWidget); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/settings.dart b/frontend/appflowy_flutter/integration_test/util/settings.dart index 76da12966c..4b01c2d791 100644 --- a/frontend/appflowy_flutter/integration_test/util/settings.dart +++ b/frontend/appflowy_flutter/integration_test/util/settings.dart @@ -39,7 +39,7 @@ extension AppFlowySettings on WidgetTester { return; } - /// Open the page taht insides the settings page + /// Open the page that insides the settings page Future openSettingsPage(SettingsPage page) async { final button = find.text(page.name, findRichText: true); expect(button, findsOneWidget); @@ -49,25 +49,25 @@ extension AppFlowySettings on WidgetTester { /// Restore the AppFlowy data storage location Future restoreLocation() async { - final buton = + final button = find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr()); - expect(buton, findsOneWidget); - await tapButton(buton); + expect(button, findsOneWidget); + await tapButton(button); return; } Future tapOpenFolderButton() async { - final buton = find.text(LocaleKeys.settings_files_open.tr()); - expect(buton, findsOneWidget); - await tapButton(buton); + final button = find.text(LocaleKeys.settings_files_open.tr()); + expect(button, findsOneWidget); + await tapButton(button); return; } Future tapCustomLocationButton() async { - final buton = + final button = find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr()); - expect(buton, findsOneWidget); - await tapButton(buton); + expect(button, findsOneWidget); + await tapButton(button); return; } diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index c5e6758eb6..385c983d7f 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -359,7 +359,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy; + PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -483,7 +483,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy; + PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -502,7 +502,7 @@ ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy; + PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index dac75a6e52..6cdbc964e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart'; import 'package:appflowy/plugins/database_view/application/view/view_cache.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart'; @@ -18,11 +19,6 @@ import 'layout/layout_setting_listener.dart'; import 'row/row_cache.dart'; import 'group/group_listener.dart'; -typedef OnRowsChanged = void Function( - List rowInfos, - RowsChangedReason, -); - typedef OnGroupByField = void Function(List); typedef OnUpdateGroup = void Function(List); typedef OnDeleteGroup = void Function(List); @@ -52,16 +48,29 @@ class LayoutCallbacks { }); } +class CalendarLayoutCallbacks { + final void Function(LayoutSettingPB) onCalendarLayoutChanged; + + CalendarLayoutCallbacks({required this.onCalendarLayoutChanged}); +} + class DatabaseCallbacks { OnDatabaseChanged? onDatabaseChanged; - OnRowsChanged? onRowsChanged; OnFieldsChanged? onFieldsChanged; OnFiltersChanged? onFiltersChanged; + OnRowsChanged? onRowsChanged; + OnRowsDeleted? onRowsDeleted; + OnRowsUpdated? onRowsUpdated; + OnRowsCreated? onRowsCreated; + DatabaseCallbacks({ this.onDatabaseChanged, this.onRowsChanged, this.onFieldsChanged, this.onFiltersChanged, + this.onRowsUpdated, + this.onRowsDeleted, + this.onRowsCreated, }); } @@ -76,21 +85,23 @@ class DatabaseController { DatabaseCallbacks? _databaseCallbacks; GroupCallbacks? _groupCallbacks; LayoutCallbacks? _layoutCallbacks; + CalendarLayoutCallbacks? _calendarLayoutCallbacks; // Getters - List get rowInfos => _viewCache.rowInfos; RowCache get rowCache => _viewCache.rowCache; // Listener final DatabaseGroupListener groupListener; final DatabaseLayoutListener layoutListener; + final DatabaseCalendarLayoutListener calendarLayoutListener; DatabaseController({required ViewPB view, required this.layoutType}) : viewId = view.id, _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id), fieldController = FieldController(viewId: view.id), groupListener = DatabaseGroupListener(view.id), - layoutListener = DatabaseLayoutListener(view.id) { + layoutListener = DatabaseLayoutListener(view.id), + calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) { _viewCache = DatabaseViewCache( viewId: viewId, fieldController: fieldController, @@ -99,16 +110,21 @@ class DatabaseController { _listenOnFieldsChanged(); _listenOnGroupChanged(); _listenOnLayoutChanged(); + if (layoutType == LayoutTypePB.Calendar) { + _listenOnCalendarLayoutChanged(); + } } void addListener({ DatabaseCallbacks? onDatabaseChanged, LayoutCallbacks? onLayoutChanged, GroupCallbacks? onGroupChanged, + CalendarLayoutCallbacks? onCalendarLayoutChanged, }) { _layoutCallbacks = onLayoutChanged; _databaseCallbacks = onDatabaseChanged; _groupCallbacks = onGroupChanged; + _calendarLayoutCallbacks = onCalendarLayoutChanged; } Future> open() async { @@ -218,9 +234,17 @@ class DatabaseController { } void _listenOnRowsChanged() { - _viewCache.addListener(onRowsChanged: (reason) { - _databaseCallbacks?.onRowsChanged?.call(rowInfos, reason); + final callbacks = + DatabaseViewCallbacks(onRowsChanged: (rows, rowByRowId, reason) { + _databaseCallbacks?.onRowsChanged?.call(rows, rowByRowId, reason); + }, onRowsDeleted: (ids) { + _databaseCallbacks?.onRowsDeleted?.call(ids); + }, onRowsUpdated: (ids) { + _databaseCallbacks?.onRowsUpdated?.call(ids); + }, onRowsCreated: (ids) { + _databaseCallbacks?.onRowsCreated?.call(ids); }); + _viewCache.addListener(callbacks); } void _listenOnFieldsChanged() { @@ -266,6 +290,14 @@ class DatabaseController { }, (r) => Log.error(r)); }); } + + void _listenOnCalendarLayoutChanged() { + calendarLayoutListener.start(onCalendarLayoutChanged: (result) { + result.fold((l) { + _calendarLayoutCallbacks?.onCalendarLayoutChanged(l); + }, (r) => Log.error(r)); + }); + } } class RowDataBuilder { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart index d724028e63..e45a428db7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -10,9 +10,14 @@ import 'row/row_cache.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnFiltersChanged = void Function(List); typedef OnDatabaseChanged = void Function(DatabasePB); + +typedef OnRowsCreated = void Function(List ids); +typedef OnRowsUpdated = void Function(List ids); +typedef OnRowsDeleted = void Function(List ids); typedef OnRowsChanged = void Function( - List, - RowsChangedReason, + UnmodifiableListView rows, + UnmodifiableMapView rowByRowId, + RowsChangedReason reason, ); typedef OnError = void Function(FlowyError); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart index 6904835b24..231582621e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart @@ -162,7 +162,7 @@ class FieldController { //Listen on setting changes _listenOnSettingChanges(); - //Listen on the fitler changes + //Listen on the filter changes _listenOnFilterChanges(); //Listen on the sort changes @@ -177,7 +177,7 @@ class FieldController { } void _listenOnFilterChanges() { - //Listen on the fitler changes + //Listen on the filter changes deleteFilterFromChangeset( List filters, @@ -230,7 +230,7 @@ class FieldController { .removeWhere((key, value) => value.id == updatedFilter.filterId); } - // Insert the filter if there is a fitler and its field info is + // Insert the filter if there is a filter and its field info is // not null if (updatedFilter.hasFilter()) { final fieldInfo = _findFieldInfo( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart new file mode 100644 index 0000000000..9ec8c1f656 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart @@ -0,0 +1,49 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:dartz/dartz.dart'; + +typedef NewLayoutFieldValue = Either; + +class DatabaseCalendarLayoutListener { + final String viewId; + PublishNotifier? _newLayoutFieldNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + DatabaseCalendarLayoutListener(this.viewId); + + void start( + {required void Function(NewLayoutFieldValue) onCalendarLayoutChanged}) { + _newLayoutFieldNotifier?.addPublishListener(onCalendarLayoutChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidSetNewLayoutField: + result.fold( + (payload) => _newLayoutFieldNotifier?.value = + left(LayoutSettingPB.fromBuffer(payload)), + (error) => _newLayoutFieldNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _newLayoutFieldNotifier?.dispose(); + _newLayoutFieldNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index 7f0265ccc7..608cc7b907 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -37,11 +37,15 @@ class RowCache { final RowCacheDelegate _delegate; final RowChangesetNotifier _rowChangeReasonNotifier; - UnmodifiableListView get visibleRows { + UnmodifiableListView get rowInfos { var visibleRows = [..._rowList.rows]; return UnmodifiableListView(visibleRows); } + UnmodifiableMapView get rowByRowId { + return UnmodifiableMapView(_rowList.rowInfoByRowId); + } + CellCache get cellCache => _cellCache; RowCache({ @@ -61,6 +65,10 @@ class RowCache { }); } + RowInfo? getRow(String rowId) { + return _rowList.get(rowId); + } + void setInitialRows(List rows) { for (final row in rows) { final rowInfo = buildGridRow(row); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart index bd163cce58..f6d4114495 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart @@ -9,14 +9,14 @@ class RowList { List get rows => List.from(_rowInfos); /// Use Map for faster access the raw row data. - final HashMap _rowInfoByRowId = HashMap(); + final HashMap rowInfoByRowId = HashMap(); RowInfo? get(String rowId) { - return _rowInfoByRowId[rowId]; + return rowInfoByRowId[rowId]; } int? indexOfRow(String rowId) { - final rowInfo = _rowInfoByRowId[rowId]; + final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { return _rowInfos.indexOf(rowInfo); } @@ -33,7 +33,7 @@ class RowList { } else { _rowInfos.add(rowInfo); } - _rowInfoByRowId[rowId] = rowInfo; + rowInfoByRowId[rowId] = rowInfo; } InsertedIndex? insert(int index, RowInfo rowInfo) { @@ -47,21 +47,21 @@ class RowList { if (oldRowInfo != null) { _rowInfos.insert(insertedIndex, rowInfo); _rowInfos.remove(oldRowInfo); - _rowInfoByRowId[rowId] = rowInfo; + rowInfoByRowId[rowId] = rowInfo; return null; } else { _rowInfos.insert(insertedIndex, rowInfo); - _rowInfoByRowId[rowId] = rowInfo; + rowInfoByRowId[rowId] = rowInfo; return InsertedIndex(index: insertedIndex, rowId: rowId); } } DeletedIndex? remove(String rowId) { - final rowInfo = _rowInfoByRowId[rowId]; + final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { final index = _rowInfos.indexOf(rowInfo); if (index != -1) { - _rowInfoByRowId.remove(rowInfo.rowPB.id); + rowInfoByRowId.remove(rowInfo.rowPB.id); _rowInfos.remove(rowInfo); } return DeletedIndex(index: index, rowInfo: rowInfo); @@ -105,7 +105,7 @@ class RowList { if (deletedRowByRowId[rowInfo.rowPB.id] == null) { newRows.add(rowInfo); } else { - _rowInfoByRowId.remove(rowInfo.rowPB.id); + rowInfoByRowId.remove(rowInfo.rowPB.id); deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo)); } }); @@ -136,7 +136,7 @@ class RowList { _rowInfos.clear(); for (final rowId in rowIds) { - final rowInfo = _rowInfoByRowId[rowId]; + final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { _rowInfos.add(rowInfo); } @@ -155,6 +155,6 @@ class RowList { } bool contains(String rowId) { - return _rowInfoByRowId[rowId] != null; + return rowInfoByRowId[rowId] != null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart index cb63463149..a2b687332c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart @@ -27,7 +27,7 @@ class SettingController { ); }); - // Listen on the seting changes + // Listen on the setting changes _listener.start(onSettingUpdated: (result) { result.fold( (newSetting) => updateSetting(newSetting), @@ -36,7 +36,7 @@ class SettingController { }); } - void startListeing({ + void startListening({ required OnSettingUpdated onSettingUpdated, required OnError onError, }) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart index 6f58c3fbde..002bc40ec0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart @@ -1,22 +1,50 @@ import 'dart:async'; +import 'dart:collection'; import 'package:appflowy_backend/log.dart'; +import '../defines.dart'; import '../field/field_controller.dart'; import '../row/row_cache.dart'; import 'view_listener.dart'; +class DatabaseViewCallbacks { + /// Will get called when number of rows were changed that includes + /// update/delete/insert rows. The [onRowsChanged] will return all + /// the rows of the current database + final OnRowsChanged? onRowsChanged; + + // Will get called when creating new rows + final OnRowsCreated? onRowsCreated; + + /// Will get called when number of rows were updated + final OnRowsUpdated? onRowsUpdated; + + /// Will get called when number of rows were deleted + final OnRowsDeleted? onRowsDeleted; + + const DatabaseViewCallbacks({ + this.onRowsChanged, + this.onRowsCreated, + this.onRowsUpdated, + this.onRowsDeleted, + }); +} + /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information class DatabaseViewCache { final String viewId; late RowCache _rowCache; - final DatabaseViewListener _gridViewListener; + final DatabaseViewListener _databaseViewListener; + DatabaseViewCallbacks? _callbacks; - List get rowInfos => _rowCache.visibleRows; + UnmodifiableListView get rowInfos => _rowCache.rowInfos; RowCache get rowCache => _rowCache; + RowInfo? getRow(String rowId) => _rowCache.getRow(rowId); + DatabaseViewCache({ required this.viewId, required FieldController fieldController, - }) : _gridViewListener = DatabaseViewListener(viewId: viewId) { + }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) { final delegate = RowDelegatesImpl(fieldController); _rowCache = RowCache( viewId: viewId, @@ -24,10 +52,28 @@ class DatabaseViewCache { cacheDelegate: delegate, ); - _gridViewListener.start( + _databaseViewListener.start( onRowsChanged: (result) { result.fold( - (changeset) => _rowCache.applyRowsChanged(changeset), + (changeset) { + // Update the cache + _rowCache.applyRowsChanged(changeset); + + if (changeset.deletedRows.isNotEmpty) { + _callbacks?.onRowsDeleted?.call(changeset.deletedRows); + } + + if (changeset.updatedRows.isNotEmpty) { + _callbacks?.onRowsUpdated + ?.call(changeset.updatedRows.map((e) => e.row.id).toList()); + } + + if (changeset.insertedRows.isNotEmpty) { + _callbacks?.onRowsCreated?.call(changeset.insertedRows + .map((insertedRow) => insertedRow.row.id) + .toList()); + } + }, (err) => Log.error(err), ); }, @@ -50,23 +96,22 @@ class DatabaseViewCache { ); }, ); + + _rowCache.onRowsChanged( + (reason) => _callbacks?.onRowsChanged?.call( + rowInfos, + _rowCache.rowByRowId, + reason, + ), + ); } Future dispose() async { - await _gridViewListener.stop(); + await _databaseViewListener.stop(); await _rowCache.dispose(); } - void addListener({ - required void Function(RowsChangedReason) onRowsChanged, - bool Function()? listenWhen, - }) { - _rowCache.onRowsChanged((reason) { - if (listenWhen != null && listenWhen() == false) { - return; - } - - onRowsChanged(reason); - }); + void addListener(DatabaseViewCallbacks callbacks) { + _callbacks = callbacks; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index bf0e1ab8f6..f0cfb43b48 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -17,9 +17,11 @@ part 'calendar_bloc.freezed.dart'; class CalendarBloc extends Bloc { final DatabaseController _databaseController; + Map fieldInfoByFieldId = {}; // Getters String get viewId => _databaseController.viewId; + FieldController get fieldController => _databaseController.fieldController; CellCache get cellCache => _databaseController.rowCache.cellCache; RowCache get rowCache => _databaseController.rowCache; @@ -28,7 +30,7 @@ class CalendarBloc extends Bloc { view: view, layoutType: LayoutTypePB.Calendar, ), - super(CalendarState.initial(view.id)) { + super(CalendarState.initial()) { on( (event, emit) async { await event.when( @@ -44,16 +46,49 @@ class CalendarBloc extends Bloc { emit(state.copyWith(database: Some(database))); }, didLoadAllEvents: (events) { - emit(state.copyWith(events: events)); + emit(state.copyWith(initialEvents: events, allEvents: events)); + }, + didReceiveNewLayoutField: (CalendarLayoutSettingsPB layoutSettings) { + _loadAllEvents(); + emit(state.copyWith(settings: Some(layoutSettings))); }, createEvent: (DateTime date, String title) async { await _createEvent(date, title); }, - didReceiveEvent: (CalendarEventData newEvent) { - emit(state.copyWith(events: [...state.events, newEvent])); + updateCalendarLayoutSetting: + (CalendarLayoutSettingsPB layoutSetting) async { + await _updateCalendarLayoutSetting(layoutSetting); }, - didUpdateFieldInfos: (Map fieldInfoByFieldId) { - emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId)); + didUpdateEvent: (CalendarEventData eventData) { + var allEvents = [...state.allEvents]; + final index = allEvents.indexWhere( + (element) => element.event!.cellId == eventData.event!.cellId, + ); + if (index != -1) { + allEvents[index] = eventData; + } + emit(state.copyWith( + allEvents: allEvents, + updateEvent: eventData, + )); + }, + didReceiveNewEvent: (CalendarEventData event) { + emit(state.copyWith( + allEvents: [...state.allEvents, event], + newEvent: event, + )); + }, + didDeleteEvents: (List deletedRowIds) { + var events = [...state.allEvents]; + events.retainWhere( + (element) => !deletedRowIds.contains(element.event!.cellId.rowId), + ); + emit( + state.copyWith( + allEvents: events, + deleteEventIds: deletedRowIds, + ), + ); }, ); }, @@ -97,7 +132,7 @@ class CalendarBloc extends Bloc { } Future _createEvent(DateTime date, String title) async { - state.settings.fold( + return state.settings.fold( () => null, (settings) async { final dateField = _getCalendarFieldInfo(settings.layoutFieldId); @@ -110,8 +145,8 @@ class CalendarBloc extends Bloc { }, ); - result.fold( - (newRow) => _loadEvent(newRow.id), + return result.fold( + (newRow) {}, (err) => Log.error(err), ); } @@ -119,17 +154,23 @@ class CalendarBloc extends Bloc { ); } - Future _loadEvent(String rowId) async { + Future _updateCalendarLayoutSetting( + CalendarLayoutSettingsPB layoutSetting) async { + return _databaseController.updateCalenderLayoutSetting(layoutSetting); + } + + Future?> _loadEvent(String rowId) async { final payload = RowIdPB(viewId: viewId, rowId: rowId); - DatabaseEventGetCalendarEvent(payload).send().then((result) { - result.fold( + return DatabaseEventGetCalendarEvent(payload).send().then((result) { + return result.fold( (eventPB) { final calendarEvent = _calendarEventDataFromEventPB(eventPB); - if (calendarEvent != null) { - add(CalendarEvent.didReceiveEvent(calendarEvent)); - } + return calendarEvent; + }, + (r) { + Log.error(r); + return null; }, - (r) => Log.error(r), ); }); } @@ -140,7 +181,7 @@ class CalendarBloc extends Bloc { result.fold( (events) { if (!isClosed) { - final calendarEvents = >[]; + final calendarEvents = >[]; for (final eventPB in events.items) { final calendarEvent = _calendarEventDataFromEventPB(eventPB); if (calendarEvent != null) { @@ -156,9 +197,9 @@ class CalendarBloc extends Bloc { }); } - CalendarEventData? _calendarEventDataFromEventPB( + CalendarEventData? _calendarEventDataFromEventPB( CalendarEventPB eventPB) { - final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId]; + final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId]; if (fieldInfo != null) { final cellId = CellIdentifier( viewId: viewId, @@ -166,7 +207,7 @@ class CalendarBloc extends Bloc { fieldInfo: fieldInfo, ); - final eventData = CalendarCardData( + final eventData = CalendarDayEvent( event: eventPB, cellId: cellId, ); @@ -192,10 +233,31 @@ class CalendarBloc extends Bloc { }, onFieldsChanged: (fieldInfos) { if (isClosed) return; - final fieldInfoByFieldId = { + fieldInfoByFieldId = { for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo }; - add(CalendarEvent.didUpdateFieldInfos(fieldInfoByFieldId)); + }, + onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}), + onRowsCreated: ((ids) async { + for (final id in ids) { + final event = await _loadEvent(id); + if (event != null && !isClosed) { + add(CalendarEvent.didReceiveNewEvent(event)); + } + } + }), + onRowsDeleted: (ids) { + if (isClosed) return; + add(CalendarEvent.didDeleteEvents(ids)); + }, + onRowsUpdated: (ids) async { + if (isClosed) return; + for (final id in ids) { + final event = await _loadEvent(id); + if (event != null) { + add(CalendarEvent.didUpdateEvent(event)); + } + } }, ); @@ -204,9 +266,13 @@ class CalendarBloc extends Bloc { onLoadLayout: _didReceiveLayoutSetting, ); + final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks( + onCalendarLayoutChanged: _didReceiveNewLayoutField); + _databaseController.addListener( onDatabaseChanged: onDatabaseChanged, onLayoutChanged: onLayoutChanged, + onCalendarLayoutChanged: onCalendarLayoutFieldChanged, ); } @@ -216,44 +282,75 @@ class CalendarBloc extends Bloc { add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar)); } } + + void _didReceiveNewLayoutField(LayoutSettingPB layoutSetting) { + if (layoutSetting.hasCalendar()) { + if (isClosed) return; + add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar)); + } + } } -typedef Events = List>; +typedef Events = List>; @freezed class CalendarEvent with _$CalendarEvent { const factory CalendarEvent.initial() = _InitialCalendar; + + // Called after loading the calendar layout setting from the backend const factory CalendarEvent.didReceiveCalendarSettings( CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings; + + // Called after loading all the current evnets const factory CalendarEvent.didLoadAllEvents(Events events) = _ReceiveCalendarEvents; - const factory CalendarEvent.didReceiveEvent( - CalendarEventData event) = _ReceiveEvent; - const factory CalendarEvent.didUpdateFieldInfos( - Map fieldInfoByFieldId) = _DidUpdateFieldInfos; + + // Called when specific event was updated + const factory CalendarEvent.didUpdateEvent( + CalendarEventData event) = _DidUpdateEvent; + + // Called after creating a new event + const factory CalendarEvent.didReceiveNewEvent( + CalendarEventData event) = _DidReceiveNewEvent; + + // Called when deleting events + const factory CalendarEvent.didDeleteEvents(List rowIds) = + _DidDeleteEvents; + + // Called when creating a new event const factory CalendarEvent.createEvent(DateTime date, String title) = _CreateEvent; + + // Called when updating the calendar's layout settings + const factory CalendarEvent.updateCalendarLayoutSetting( + CalendarLayoutSettingsPB layoutSetting) = _UpdateCalendarLayoutSetting; + const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = _ReceiveDatabaseUpdate; + + const factory CalendarEvent.didReceiveNewLayoutField( + CalendarLayoutSettingsPB layoutSettings) = _DidReceiveNewLayoutField; } @freezed class CalendarState with _$CalendarState { const factory CalendarState({ - required String databaseId, required Option database, - required Events events, - required Map fieldInfoByFieldId, + required Events allEvents, + required Events initialEvents, + CalendarEventData? newEvent, + required List deleteEventIds, + CalendarEventData? updateEvent, required Option settings, required DatabaseLoadingState loadingState, required Option noneOrError, }) = _CalendarState; - factory CalendarState.initial(String databaseId) => CalendarState( + factory CalendarState.initial() => CalendarState( database: none(), - databaseId: databaseId, - fieldInfoByFieldId: {}, - events: [], + allEvents: [], + initialEvents: [], + deleteEventIds: [], settings: none(), noneOrError: none(), loadingState: const _Loading(), @@ -277,8 +374,10 @@ class CalendarEditingRow { }); } -class CalendarCardData { +class CalendarDayEvent { final CalendarEventPB event; final CellIdentifier cellId; - CalendarCardData({required this.cellId, required this.event}); + + String get eventId => cellId.rowId; + CalendarDayEvent({required this.cellId, required this.event}); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart new file mode 100644 index 0000000000..1ebc97d90a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart @@ -0,0 +1,55 @@ +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:bloc/bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'calendar_setting_bloc.freezed.dart'; + +typedef DayOfWeek = int; + +class CalendarSettingBloc + extends Bloc { + CalendarSettingBloc({required CalendarLayoutSettingsPB? layoutSettings}) + : super(CalendarSettingState.initial(layoutSettings)) { + on((event, emit) { + event.when( + performAction: (action) { + emit(state.copyWith(selectedAction: Some(action))); + }, + updateLayoutSetting: (setting) { + emit(state.copyWith(layoutSetting: Some(setting))); + }, + ); + }); + } + + @override + Future close() async => super.close(); +} + +@freezed +class CalendarSettingState with _$CalendarSettingState { + const factory CalendarSettingState({ + required Option selectedAction, + required Option layoutSetting, + }) = _CalendarSettingState; + + factory CalendarSettingState.initial( + CalendarLayoutSettingsPB? layoutSettings) => + CalendarSettingState( + selectedAction: none(), + layoutSetting: layoutSettings == null ? none() : Some(layoutSettings), + ); +} + +@freezed +class CalendarSettingEvent with _$CalendarSettingEvent { + const factory CalendarSettingEvent.performAction( + CalendarSettingAction action) = _PerformAction; + const factory CalendarSettingEvent.updateLayoutSetting( + CalendarLayoutSettingsPB setting) = _UpdateLayoutSetting; +} + +enum CalendarSettingAction { + layout, +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart index a7ce510565..1ddfb7a64f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart @@ -34,7 +34,7 @@ class CalendarPluginBuilder extends PluginBuilder { class CalendarPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class CalendarPlugin extends Plugin { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart new file mode 100644 index 0000000000..3a71c2c4ff --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart @@ -0,0 +1,267 @@ +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../grid/presentation/layout/sizes.dart'; +import '../application/calendar_bloc.dart'; + +class CalendarDayCard extends StatelessWidget { + final String viewId; + final bool isToday; + final bool isInMonth; + final DateTime date; + final RowCache _rowCache; + final CardCellBuilder _cellBuilder; + final List events; + final void Function(DateTime) onCreateEvent; + + CalendarDayCard({ + required this.viewId, + required this.isToday, + required this.isInMonth, + required this.date, + required this.onCreateEvent, + required RowCache rowCache, + required this.events, + Key? key, + }) : _rowCache = rowCache, + _cellBuilder = CardCellBuilder(rowCache.cellCache), + super(key: key); + + @override + Widget build(BuildContext context) { + Color backgroundColor = Theme.of(context).colorScheme.surface; + if (!isInMonth) { + backgroundColor = AFThemeExtension.of(context).lightGreyHover; + } + + return ChangeNotifierProvider( + create: (_) => _CardEnterNotifier(), + builder: ((context, child) { + final children = events.map((event) { + return _DayEventCell( + event: event, + viewId: viewId, + onClick: () => _showRowDetailPage(event, context), + child: _cellBuilder.buildCell( + cellId: event.cellId, + styles: {FieldType.RichText: TextCardCellStyle(10)}, + ), + ); + }).toList(); + + final child = Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _Header( + date: date, + isInMonth: isInMonth, + isToday: isToday, + onCreate: () => onCreateEvent(date), + ), + VSpace(GridSize.typeOptionSeparatorHeight), + Flexible( + child: ListView.separated( + itemBuilder: (BuildContext context, int index) { + return children[index]; + }, + itemCount: children.length, + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + ), + ), + ], + )); + + return Container( + color: backgroundColor, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => notifyEnter(context, true), + onExit: (p) => notifyEnter(context, false), + child: child, + ), + ); + }), + ); + } + + void _showRowDetailPage(CalendarDayEvent event, BuildContext context) { + final dataController = RowController( + rowId: event.cellId.rowId, + viewId: viewId, + rowCache: _rowCache, + ); + + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: GridCellBuilder( + cellCache: _rowCache.cellCache, + ), + dataController: dataController, + ); + }, + ); + } + + notifyEnter(BuildContext context, bool isEnter) { + Provider.of<_CardEnterNotifier>( + context, + listen: false, + ).onEnter = isEnter; + } +} + +class _DayEventCell extends StatelessWidget { + final String viewId; + final CalendarDayEvent event; + final VoidCallback onClick; + final Widget child; + const _DayEventCell({ + required this.viewId, + required this.event, + required this.onClick, + required this.child, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyHover( + child: GestureDetector( + onTap: onClick, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: child, + ), + ), + ); + } +} + +class _Header extends StatelessWidget { + final bool isToday; + final bool isInMonth; + final DateTime date; + final VoidCallback onCreate; + const _Header({ + required this.isToday, + required this.isInMonth, + required this.date, + required this.onCreate, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer<_CardEnterNotifier>( + builder: (context, notifier, _) { + final badge = _DayBadge( + isToday: isToday, + isInMonth: isInMonth, + date: date, + ); + return Row( + children: [ + if (notifier.onEnter) _NewEventButton(onClick: onCreate), + const Spacer(), + badge, + ], + ); + }, + ); + } +} + +class _NewEventButton extends StatelessWidget { + final VoidCallback onClick; + const _NewEventButton({ + required this.onClick, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + onPressed: onClick, + iconPadding: EdgeInsets.zero, + icon: svgWidget( + "home/add", + color: Theme.of(context).colorScheme.onSurface, + ), + width: 22, + ); + } +} + +class _DayBadge extends StatelessWidget { + final bool isToday; + final bool isInMonth; + final DateTime date; + const _DayBadge({ + required this.isToday, + required this.isInMonth, + required this.date, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Color dayTextColor = Theme.of(context).colorScheme.onSurface; + String dayString = date.day == 1 + ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date) + : date.day.toString(); + + if (isToday) { + dayTextColor = Theme.of(context).colorScheme.onPrimary; + } + if (!isInMonth) { + dayTextColor = Theme.of(context).disabledColor; + } + + Widget day = Container( + decoration: BoxDecoration( + color: isToday ? Theme.of(context).colorScheme.primary : null, + borderRadius: Corners.s6Border, + ), + padding: GridSize.typeOptionContentInsets, + child: FlowyText.medium( + dayString, + color: dayTextColor, + ), + ); + + return day; + } +} + +class _CardEnterNotifier extends ChangeNotifier { + bool _onEnter = false; + + _CardEnterNotifier(); + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index 0eac7870a3..988654a8ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -1,22 +1,15 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart'; -import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; -import '../../grid/presentation/layout/sizes.dart'; -import '../../widgets/row/cell_builder.dart'; -import '../../widgets/row/row_detail.dart'; +import 'calendar_day.dart'; import 'layout/sizes.dart'; import 'toolbar/calendar_toolbar.dart'; @@ -29,7 +22,7 @@ class CalendarPage extends StatefulWidget { } class _CalendarPageState extends State { - final _eventController = EventController(); + final _eventController = EventController(); GlobalKey? _calendarState; late CalendarBloc _calendarBloc; @@ -58,21 +51,55 @@ class _CalendarPageState extends State { value: _calendarBloc, ) ], - child: BlocListener( - listenWhen: (previous, current) => previous.events != current.events, - listener: (context, state) { - if (state.events.isNotEmpty) { - _eventController.removeWhere((element) => true); - _eventController.addAll(state.events); - } - }, + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.initialEvents != c.initialEvents, + listener: (context, state) { + _eventController.removeWhere((_) => true); + _eventController.addAll(state.initialEvents); + }, + ), + BlocListener( + listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds, + listener: (context, state) { + _eventController.removeWhere( + (element) => + state.deleteEventIds.contains(element.event!.eventId), + ); + }, + ), + BlocListener( + listenWhen: (p, c) => p.updateEvent != c.updateEvent, + listener: (context, state) { + if (state.updateEvent != null) { + _eventController.removeWhere((element) => + state.updateEvent!.event!.eventId == + element.event!.eventId); + _eventController.add(state.updateEvent!); + } + }, + ), + BlocListener( + listenWhen: (p, c) => p.newEvent != c.newEvent, + listener: (context, state) { + if (state.newEvent != null) { + _eventController.add(state.newEvent!); + } + }, + ), + ], child: BlocBuilder( builder: (context, state) { return Column( children: [ // const _ToolbarBlocAdaptor(), - _toolbar(), - _buildCalendar(_eventController), + const CalendarToolbar(), + _buildCalendar( + _eventController, + state.settings + .foldLeft(0, (previous, a) => a.firstDayOfWeek), + ), ], ); }, @@ -82,16 +109,13 @@ class _CalendarPageState extends State { ); } - Widget _toolbar() { - return const CalendarToolbar(); - } - - Widget _buildCalendar(EventController eventController) { + Widget _buildCalendar(EventController eventController, int firstDayOfWeek) { return Expanded( child: MonthView( key: _calendarState, controller: _eventController, - cellAspectRatio: 1.75, + cellAspectRatio: .9, + startDay: _weekdayFromInt(firstDayOfWeek), borderColor: Theme.of(context).dividerColor, headerBuilder: _headerNavigatorBuilder, weekDayBuilder: _headerWeekDayBuilder, @@ -154,47 +178,19 @@ class _CalendarPageState extends State { Widget _calendarDayBuilder( DateTime date, - List> calenderEvents, + List> calenderEvents, isToday, isInMonth, ) { - final builder = CardCellBuilder(_calendarBloc.cellCache); - final cells = calenderEvents.map((value) => value.event!).map((event) { - final child = builder.buildCell(cellId: event.cellId); + final events = calenderEvents.map((value) => value.event!).toList(); - return FlowyHover( - child: GestureDetector( - onTap: () { - final dataController = RowController( - rowId: event.cellId.rowId, - viewId: widget.view.id, - rowCache: _calendarBloc.rowCache, - ); - - FlowyOverlay.show( - context: context, - builder: (BuildContext context) { - return RowDetailPage( - cellBuilder: - GridCellBuilder(cellCache: _calendarBloc.cellCache), - dataController: dataController, - ); - }, - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: child, - ), - ), - ); - }).toList(); - - return _CalendarCard( + return CalendarDayCard( + viewId: widget.view.id, isToday: isToday, isInMonth: isInMonth, + events: events, date: date, - children: cells, + rowCache: _calendarBloc.rowCache, onCreateEvent: (date) { _calendarBloc.add( CalendarEvent.createEvent( @@ -205,175 +201,9 @@ class _CalendarPageState extends State { }, ); } -} -class _CalendarCard extends StatelessWidget { - final bool isToday; - final bool isInMonth; - final DateTime date; - final List children; - final void Function(DateTime) onCreateEvent; - - const _CalendarCard({ - required this.isToday, - required this.isInMonth, - required this.date, - required this.children, - required this.onCreateEvent, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - Color backgroundColor = Theme.of(context).colorScheme.surface; - if (!isInMonth) { - backgroundColor = AFThemeExtension.of(context).lightGreyHover; - } - - return ChangeNotifierProvider( - create: (_) => _CardEnterNotifier(), - builder: ((context, child) { - return Container( - color: backgroundColor, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (p) => notifyEnter(context, true), - onExit: (p) => notifyEnter(context, false), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - _Header( - date: date, - isInMonth: isInMonth, - isToday: isToday, - onCreate: () => onCreateEvent(date), - ), - ...children - ], - ), - ), - ), - ); - }), - ); - } - - notifyEnter(BuildContext context, bool isEnter) { - Provider.of<_CardEnterNotifier>( - context, - listen: false, - ).onEnter = isEnter; + WeekDays _weekdayFromInt(int dayOfWeek) { + // MonthView places the first day of week on the second column for some reason. + return WeekDays.values[(dayOfWeek + 1) % 7]; } } - -class _Header extends StatelessWidget { - final bool isToday; - final bool isInMonth; - final DateTime date; - final VoidCallback onCreate; - const _Header({ - required this.isToday, - required this.isInMonth, - required this.date, - required this.onCreate, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Consumer<_CardEnterNotifier>( - builder: (context, notifier, _) { - final badge = _DayBadge( - isToday: isToday, - isInMonth: isInMonth, - date: date, - ); - return Row( - children: [ - if (notifier.onEnter) _NewEventButton(onClick: onCreate), - const Spacer(), - badge, - ], - ); - }, - ); - } -} - -class _NewEventButton extends StatelessWidget { - final VoidCallback onClick; - const _NewEventButton({ - required this.onClick, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - onPressed: onClick, - iconPadding: EdgeInsets.zero, - icon: svgWidget( - "home/add", - color: Theme.of(context).colorScheme.onSurface, - ), - width: 22, - ); - } -} - -class _DayBadge extends StatelessWidget { - final bool isToday; - final bool isInMonth; - final DateTime date; - const _DayBadge({ - required this.isToday, - required this.isInMonth, - required this.date, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - Color dayTextColor = Theme.of(context).colorScheme.onSurface; - String dayString = date.day == 1 - ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date) - : date.day.toString(); - - if (isToday) { - dayTextColor = Theme.of(context).colorScheme.onPrimary; - } - if (!isInMonth) { - dayTextColor = Theme.of(context).disabledColor; - } - - Widget day = Container( - decoration: BoxDecoration( - color: isToday ? Theme.of(context).colorScheme.primary : null, - borderRadius: Corners.s6Border, - ), - padding: GridSize.typeOptionContentInsets, - child: FlowyText.medium( - dayString, - color: dayTextColor, - ), - ); - - return day; - } -} - -class _CardEnterNotifier extends ChangeNotifier { - bool _onEnter = false; - - _CardEnterNotifier(); - - set onEnter(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get onEnter => _onEnter; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart new file mode 100644 index 0000000000..ff69888813 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -0,0 +1,410 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart' + hide DateFormat; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:protobuf/protobuf.dart'; + +import 'calendar_setting.dart'; + +class CalendarLayoutSetting extends StatefulWidget { + final CalendarSettingContext settingContext; + final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated; + + const CalendarLayoutSetting({ + required this.onUpdated, + required this.settingContext, + super.key, + }); + + @override + State createState() => _CalendarLayoutSettingState(); +} + +class _CalendarLayoutSettingState extends State { + late final PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final CalendarLayoutSettingsPB? settings = state.layoutSetting + .foldLeft(null, (previous, settings) => settings); + + if (settings == null) { + return const CircularProgressIndicator(); + } + final availableSettings = _availableCalendarSettings(settings); + + final items = availableSettings.map((setting) { + switch (setting) { + case CalendarLayoutSettingAction.showWeekNumber: + return ShowWeekNumber( + showWeekNumbers: settings.showWeekNumbers, + onUpdated: (showWeekNumbers) { + _updateLayoutSettings( + context, + showWeekNumbers: showWeekNumbers, + onUpdated: widget.onUpdated, + ); + }, + ); + case CalendarLayoutSettingAction.showWeekends: + return ShowWeekends( + showWeekends: settings.showWeekends, + onUpdated: (showWeekends) { + _updateLayoutSettings( + context, + showWeekends: showWeekends, + onUpdated: widget.onUpdated, + ); + }, + ); + case CalendarLayoutSettingAction.firstDayOfWeek: + return FirstDayOfWeek( + firstDayOfWeek: settings.firstDayOfWeek, + popoverMutex: popoverMutex, + onUpdated: (firstDayOfWeek) { + _updateLayoutSettings( + context, + onUpdated: widget.onUpdated, + firstDayOfWeek: firstDayOfWeek, + ); + }, + ); + case CalendarLayoutSettingAction.layoutField: + return LayoutDateField( + fieldController: widget.settingContext.fieldController, + viewId: widget.settingContext.viewId, + fieldId: settings.layoutFieldId, + popoverMutex: popoverMutex, + onUpdated: (fieldId) { + _updateLayoutSettings(context, + onUpdated: widget.onUpdated, layoutFieldId: fieldId); + }, + ); + default: + return ShowWeekends( + showWeekends: settings.showWeekends, + onUpdated: (showWeekends) { + _updateLayoutSettings(context, + onUpdated: widget.onUpdated, showWeekends: showWeekends); + }, + ); + } + }).toList(); + + return SizedBox( + width: 200, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: items.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) => items[index], + padding: const EdgeInsets.all(6.0), + ), + ); + }, + ); + } + + List _availableCalendarSettings( + CalendarLayoutSettingsPB layoutSettings) { + List settings = [ + CalendarLayoutSettingAction.layoutField, + // CalendarLayoutSettingAction.layoutType, + // CalendarLayoutSettingAction.showWeekNumber, + ]; + + switch (layoutSettings.layoutTy) { + case CalendarLayoutPB.DayLayout: + // settings.add(CalendarLayoutSettingAction.showTimeLine); + break; + case CalendarLayoutPB.MonthLayout: + settings.addAll([ + // CalendarLayoutSettingAction.showWeekends, + // if (layoutSettings.showWeekends) + CalendarLayoutSettingAction.firstDayOfWeek, + ]); + break; + case CalendarLayoutPB.WeekLayout: + settings.addAll([ + // CalendarLayoutSettingAction.showWeekends, + // if (layoutSettings.showWeekends) + CalendarLayoutSettingAction.firstDayOfWeek, + // CalendarLayoutSettingAction.showTimeLine, + ]); + break; + } + + return settings; + } + + void _updateLayoutSettings( + BuildContext context, { + required Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated, + bool? showWeekends, + bool? showWeekNumbers, + int? firstDayOfWeek, + String? layoutFieldId, + }) { + CalendarLayoutSettingsPB setting = context + .read() + .state + .layoutSetting + .foldLeft(null, (previous, settings) => settings)!; + setting.freeze(); + setting = setting.rebuild((setting) { + if (showWeekends != null) { + setting.showWeekends = !showWeekends; + } + if (showWeekNumbers != null) { + setting.showWeekNumbers = !showWeekNumbers; + } + if (firstDayOfWeek != null) { + setting.firstDayOfWeek = firstDayOfWeek; + } + if (layoutFieldId != null) { + setting.layoutFieldId = layoutFieldId; + } + }); + context + .read() + .add(CalendarSettingEvent.updateLayoutSetting(setting)); + onUpdated(setting); + } +} + +class LayoutDateField extends StatelessWidget { + final String fieldId; + final String viewId; + final FieldController fieldController; + final PopoverMutex popoverMutex; + final Function(String fieldId) onUpdated; + + const LayoutDateField({ + required this.fieldId, + required this.fieldController, + required this.viewId, + required this.popoverMutex, + required this.onUpdated, + super.key, + }); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(300, 400)), + mutex: popoverMutex, + popupBuilder: (context) { + return BlocProvider( + create: (context) => getIt( + param1: viewId, param2: fieldController) + ..add(const DatabasePropertyEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final items = state.fieldContexts + .where((field) => field.fieldType == FieldType.DateTime) + .map( + (fieldInfo) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(fieldInfo.name), + onTap: () { + onUpdated(fieldInfo.id); + popoverMutex.close(); + }, + leftIcon: svgWidget('grid/field/date'), + rightIcon: fieldInfo.id == fieldId + ? svgWidget('grid/checkmark') + : null, + ), + ); + }, + ).toList(); + + return SizedBox( + width: 200, + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => items[index], + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: items.length, + ), + ); + }, + ), + ); + }, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + text: FlowyText.medium( + LocaleKeys.calendar_settings_layoutDateField.tr()), + ), + ), + ); + } +} + +class ShowWeekNumber extends StatelessWidget { + final bool showWeekNumbers; + final Function(bool showWeekNumbers) onUpdated; + + const ShowWeekNumber({ + required this.showWeekNumbers, + required this.onUpdated, + super.key, + }); + + @override + Widget build(BuildContext context) { + return _toggleItem( + onToggle: (showWeekNumbers) { + onUpdated(!showWeekNumbers); + }, + value: showWeekNumbers, + text: LocaleKeys.calendar_settings_showWeekNumbers.tr(), + ); + } +} + +class ShowWeekends extends StatelessWidget { + final bool showWeekends; + final Function(bool showWeekends) onUpdated; + const ShowWeekends({ + super.key, + required this.showWeekends, + required this.onUpdated, + }); + + @override + Widget build(BuildContext context) { + return _toggleItem( + onToggle: (showWeekends) { + onUpdated(!showWeekends); + }, + value: showWeekends, + text: LocaleKeys.calendar_settings_showWeekends.tr(), + ); + } +} + +class FirstDayOfWeek extends StatelessWidget { + final int firstDayOfWeek; + final PopoverMutex popoverMutex; + final Function(int firstDayOfWeek) onUpdated; + const FirstDayOfWeek({ + super.key, + required this.firstDayOfWeek, + required this.onUpdated, + required this.popoverMutex, + }); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(300, 400)), + mutex: popoverMutex, + popupBuilder: (context) { + final symbols = + DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; + // starts from sunday + final items = symbols.WEEKDAYS.asMap().entries.map((entry) { + final index = (entry.key - 1) % 7; + final string = entry.value; + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(string), + onTap: () { + onUpdated(index); + popoverMutex.close(); + }, + rightIcon: + firstDayOfWeek == index ? svgWidget('grid/checkmark') : null, + ), + ); + }).toList(); + + return SizedBox( + width: 100, + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => items[index], + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: 2, + ), + ); + }, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + text: FlowyText.medium( + LocaleKeys.calendar_settings_firstDayOfWeek.tr()), + ), + ), + ); + } +} + +Widget _toggleItem({ + required String text, + required bool value, + required void Function(bool) onToggle, +}) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + child: Row( + children: [ + FlowyText.medium(text), + const Spacer(), + Toggle( + value: value, + onChanged: (value) => onToggle(!value), + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], + ), + ), + ); +} + +enum CalendarLayoutSettingAction { + layoutField, + layoutType, + showWeekends, + firstDayOfWeek, + showWeekNumber, + showTimeLine, +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart new file mode 100644 index 0000000000..d9777ebba8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart @@ -0,0 +1,112 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:styled_widget/styled_widget.dart'; + +import 'calendar_layout_setting.dart'; + +/// The highest-level widget shown in the popover triggered by clicking the +/// "Settings" button. By default, shows [AllCalendarSettings] but upon +/// selecting a category, replaces contents with contents of the submenu. +class CalendarSetting extends StatelessWidget { + final CalendarSettingContext settingContext; + final CalendarLayoutSettingsPB? layoutSettings; + final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated; + + const CalendarSetting({ + required this.onUpdated, + required this.layoutSettings, + required this.settingContext, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CalendarSettingBloc(layoutSettings: layoutSettings), + child: BlocBuilder( + builder: (context, state) { + final CalendarSettingAction? action = + state.selectedAction.foldLeft(null, (previous, action) => action); + switch (action) { + case CalendarSettingAction.layout: + return CalendarLayoutSetting( + onUpdated: onUpdated, + settingContext: settingContext, + ); + default: + return const AllCalendarSettings().padding(all: 6.0); + } + }, + ), + ); + } +} + +/// Shows all of the available categories of settings that can be set here. +/// For now, this only includes the Layout category. +class AllCalendarSettings extends StatelessWidget { + const AllCalendarSettings({super.key}); + + @override + Widget build(BuildContext context) { + final items = CalendarSettingAction.values + .map((e) => _settingItem(context, e)) + .toList(); + + return SizedBox( + width: 140, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: items.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) => items[index], + ), + ); + } + + Widget _settingItem(BuildContext context, CalendarSettingAction action) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(action.title()), + onTap: () { + context + .read() + .add(CalendarSettingEvent.performAction(action)); + }, + ), + ); + } +} + +extension _SettingExtension on CalendarSettingAction { + String title() { + switch (this) { + case CalendarSettingAction.layout: + return LocaleKeys.grid_settings_layout.tr(); + } + } +} + +class CalendarSettingContext { + final String viewId; + final FieldController fieldController; + + CalendarSettingContext({ + required this.viewId, + required this.fieldController, + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart index 4b1399763b..1f704b5e83 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart @@ -1,5 +1,14 @@ -import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../application/calendar_bloc.dart'; +import 'calendar_setting.dart'; class CalendarToolbar extends StatelessWidget { const CalendarToolbar({super.key}); @@ -10,14 +19,65 @@ class CalendarToolbar extends StatelessWidget { height: 40, child: Row( mainAxisAlignment: MainAxisAlignment.end, - children: const [ - FlowyTextButton( - "Settings", - fillColor: Colors.transparent, - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), - ), + children: [ + _SettingButton(), ], ), ); } } + +class _SettingButton extends StatefulWidget { + @override + State createState() => _SettingButtonState(); +} + +class _SettingButtonState extends State<_SettingButton> { + late PopoverController popoverController; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithRightAligned, + triggerActions: PopoverTriggerFlags.none, + constraints: BoxConstraints.loose(const Size(300, 400)), + margin: EdgeInsets.zero, + child: FlowyTextButton( + LocaleKeys.settings_title.tr(), + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.typeOptionContentInsets, + onPressed: () => popoverController.show(), + ), + popupBuilder: (BuildContext popoverContext) { + final bloc = context.watch(); + final settingContext = CalendarSettingContext( + viewId: bloc.viewId, + fieldController: bloc.fieldController, + ); + return CalendarSetting( + settingContext: settingContext, + layoutSettings: bloc.state.settings.fold( + () => null, + (settings) => settings, + ), + onUpdated: (layoutSettings) { + if (layoutSettings == null) { + return; + } + context + .read() + .add(CalendarEvent.updateCalendarLayoutSetting(layoutSettings)); + }, + ); + }, // use blocbuilder + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index c0cb7eb245..7d73ab9b4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -72,7 +72,7 @@ class GridBloc extends Bloc { add(GridEvent.didReceiveGridUpdate(database)); } }, - onRowsChanged: (rowInfos, reason) { + onRowsChanged: (rowInfos, _, reason) { if (!isClosed) { add(GridEvent.didReceiveRowUpdate(rowInfos, reason)); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart index f9cd9d104d..1b0ae521ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart @@ -87,7 +87,7 @@ class _CheckboxFilterEditorState extends State { child: BlocBuilder( builder: (context, state) { final List children = [ - _buildFilterPannel(context, state), + _buildFilterPanel(context, state), ]; return Padding( @@ -99,7 +99,7 @@ class _CheckboxFilterEditorState extends State { ); } - Widget _buildFilterPannel( + Widget _buildFilterPanel( BuildContext context, CheckboxFilterEditorState state) { return SizedBox( height: 20, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart index b05dfdaf63..3ffc07b6c9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart @@ -96,7 +96,7 @@ class _SelectOptionFilterEditorState extends State { SelectOptionFilterEditorState>( builder: (context, state) { List slivers = [ - SliverToBoxAdapter(child: _buildFilterPannel(context, state)), + SliverToBoxAdapter(child: _buildFilterPanel(context, state)), ]; if (state.filter.condition != SelectOptionConditionPB.OptionIsEmpty && @@ -131,7 +131,7 @@ class _SelectOptionFilterEditorState extends State { ); } - Widget _buildFilterPannel( + Widget _buildFilterPanel( BuildContext context, SelectOptionFilterEditorState state) { return SizedBox( height: 20, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart index 94ccd5164e..0eaebbc4cb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart @@ -94,7 +94,7 @@ class _TextFilterEditorState extends State { child: BlocBuilder( builder: (context, state) { final List children = [ - _buildFilterPannel(context, state), + _buildFilterPanel(context, state), ]; if (state.filter.condition != TextFilterConditionPB.TextIsEmpty && @@ -112,7 +112,7 @@ class _TextFilterEditorState extends State { ); } - Widget _buildFilterPannel(BuildContext context, TextFilterEditorState state) { + Widget _buildFilterPanel(BuildContext context, TextFilterEditorState state) { return SizedBox( height: 20, child: Row( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart index 29e38e7a72..ff0abf92af 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart @@ -147,6 +147,8 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> { widget.popoverMutex.listenOnPopoverChanged(() { if (focusNode.hasFocus) { focusNode.unfocus(); + } else { + focusNode.requestFocus(); } }); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart index be25a01ec8..22f3403a4d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart @@ -122,7 +122,7 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate { color: Theme.of(context).colorScheme.background, height: fixHeight, child: FlowyTextField( - hintText: LocaleKeys.grid_settings_filterBy.tr(), + hintText: LocaleKeys.grid_settings_sortBy.tr(), onChanged: (text) { context .read() diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index 4bc655a305..ddf2dee63a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -23,6 +23,7 @@ class CardCellBuilder { required CellIdentifier cellId, EditableCardNotifier? cellNotifier, CardConfiguration? cardConfiguration, + Map? styles, }) { final cellControllerBuilder = CellControllerBuilder( cellId: cellId, @@ -30,6 +31,7 @@ class CardCellBuilder { ); final key = cellId.key(); + final style = styles?[cellId.fieldType]; switch (cellId.fieldType) { case FieldType.Checkbox: return CheckboxCardCell( @@ -70,6 +72,7 @@ class CardCellBuilder { return TextCardCell( cellControllerBuilder: cellControllerBuilder, editableNotifier: cellNotifier, + style: isStyleOrNull(style), key: key, ); case FieldType.URL: diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart index 6bc9ee9eac..e5942c8ab3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart @@ -24,10 +24,21 @@ class CardConfiguration { } } -abstract class CardCell extends StatefulWidget { - final T? cardData; +abstract class CardCellStyle {} - const CardCell({super.key, this.cardData}); +S? isStyleOrNull(CardCellStyle? style) { + if (style is S) { + return style as S; + } else { + return null; + } +} + +abstract class CardCell extends StatefulWidget { + final T? cardData; + final S? style; + + const CardCell({super.key, this.cardData, this.style}); } class EditableCardNotifier { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart index 6eba4acef9..2251af0cf1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart @@ -9,7 +9,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../bloc/select_option_card_cell_bloc.dart'; import 'card_cell.dart'; -class SelectOptionCardCell extends CardCell with EditableCell { +class SelectOptionCardCellStyle extends CardCellStyle {} + +class SelectOptionCardCell extends CardCell + with EditableCell { final CellControllerBuilder cellControllerBuilder; final CellRenderHook, T>? renderHook; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart index 8ffc834247..4eb9d9137e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,7 +8,14 @@ import '../bloc/text_card_cell_bloc.dart'; import '../define.dart'; import 'card_cell.dart'; -class TextCardCell extends CardCell with EditableCell { +class TextCardCellStyle extends CardCellStyle { + final double fontSize; + + TextCardCellStyle(this.fontSize); +} + +class TextCardCell extends CardCell + with EditableCell { @override final EditableCardNotifier? editableNotifier; final CellControllerBuilder cellControllerBuilder; @@ -17,8 +23,9 @@ class TextCardCell extends CardCell with EditableCell { const TextCardCell({ required this.cellControllerBuilder, this.editableNotifier, + TextCardCellStyle? style, Key? key, - }) : super(key: key); + }) : super(key: key, style: style); @override State createState() => _TextCardCellState(); @@ -129,6 +136,14 @@ class _TextCardCellState extends State { super.dispose(); } + double _fontSize() { + if (widget.style != null) { + return widget.style!.fontSize; + } else { + return 14; + } + } + Widget _buildText(TextCardCellState state) { return Padding( padding: EdgeInsets.symmetric( @@ -136,7 +151,7 @@ class _TextCardCellState extends State { ), child: FlowyText.medium( state.content, - fontSize: 14, + fontSize: _fontSize(), maxLines: null, // Enable multiple lines ), ); @@ -150,7 +165,7 @@ class _TextCardCellState extends State { onChanged: (value) => focusChanged(), onEditingComplete: () => focusNode.unfocus(), maxLines: null, - style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14), + style: Theme.of(context).textTheme.bodyMedium!.size(_fontSize()), decoration: InputDecoration( // Magic number 4 makes the textField take up the same space as FlowyText contentPadding: EdgeInsets.symmetric( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index 0c0313c9fe..be6674b5bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -23,9 +23,8 @@ import '../../../../grid/presentation/widgets/common/type_option_separator.dart' import '../../../../grid/presentation/widgets/header/type_option/date.dart'; import 'date_cal_bloc.dart'; -final kToday = DateTime.now(); -final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day); -final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day); +final kFirstDay = DateTime.utc(1970, 1, 1); +final kLastDay = DateTime.utc(2100, 1, 1); class DateCellEditor extends StatefulWidget { final VoidCallback onDismissed; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart index 92c2f347a2..ddb4fe915d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:appflowy/plugins/document/application/share_service.dart'; import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart'; import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -53,6 +54,7 @@ class DocShareBloc extends Bloc { return documentToMarkdown(document, customParsers: [ const DividerNodeParser(), const MathEquationNodeParser(), + const CodeBlockNodeParser(), ]); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 7f54bf4d27..8c87000722 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,4 +1,7 @@ -import 'package:appflowy/plugins/document/presentation/plugins/board/board_menu_item.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/board/board_view_menu_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart'; @@ -7,19 +10,18 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:flowy_infra_ui/widget/error_page.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../startup/startup.dart'; import 'application/doc_bloc.dart'; import 'editor_styles.dart'; import 'presentation/banner.dart'; +import 'presentation/plugins/grid/grid_view_menu_item.dart'; +import 'presentation/plugins/board/board_menu_item.dart'; class DocumentPage extends StatefulWidget { final VoidCallback onDeleted; @@ -128,11 +130,11 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final autoFocusParamters = _autoFocusParamters(); + final autoFocusParameters = _autoFocusParameters(); final editor = AppFlowyEditor( editorState: editorState, - autoFocus: autoFocusParamters.value1, - focusedSelection: autoFocusParamters.value2, + autoFocus: autoFocusParameters.value1, + focusedSelection: autoFocusParameters.value2, customBuilders: { // Divider kDividerType: DividerWidgetBuilder(), @@ -172,8 +174,12 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> { emojiMenuItem, // Board boardMenuItem, + // Create Board + boardViewMenuItem(documentBloc), // Grid gridMenuItem, + // Create Grid + gridViewMenuItem(documentBloc), // Callout calloutMenuItem, // AI @@ -234,7 +240,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> { } } - dartz.Tuple2 _autoFocusParamters() { + dartz.Tuple2 _autoFocusParameters() { if (editorState.document.isEmpty) { return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0)); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart index d4e467ab27..16a286e7e3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart @@ -82,8 +82,8 @@ Iterable> customPluginTheme(BuildContext context) { }, ); final pluginTheme = Theme.of(context).brightness == Brightness.dark - ? darkPlguinStyleExtension - : lightPlguinStyleExtension; + ? darkPluginStyleExtension + : lightPluginStyleExtension; return pluginTheme.toList() ..removeWhere((element) => element is HeadingPluginStyle || element is NumberListPluginStyle) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart index fcab0eacd0..80a52a9f3e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; -const String _kDocumentAppearenceFontSize = 'kDocumentAppearenceFontSize'; +const String _kDocumentAppearanceFontSize = 'kDocumentAppearanceFontSize'; class DocumentAppearance { const DocumentAppearance({ @@ -24,7 +24,7 @@ class DocumentAppearanceCubit extends Cubit { void fetch() async { final prefs = await SharedPreferences.getInstance(); - final fontSize = prefs.getDouble(_kDocumentAppearenceFontSize) ?? 14.0; + final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 14.0; emit(state.copyWith( fontSize: fontSize, )); @@ -32,7 +32,7 @@ class DocumentAppearanceCubit extends Cubit { void syncFontSize(double fontSize) async { final prefs = await SharedPreferences.getInstance(); - prefs.setDouble(_kDocumentAppearenceFontSize, fontSize); + prefs.setDouble(_kDocumentAppearanceFontSize, fontSize); emit(state.copyWith( fontSize: fontSize, )); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart index ebce76461e..73cfbeecb4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart @@ -20,7 +20,7 @@ void showLinkToPageMenu( BuildContext context, ViewLayoutTypePB pageType, ) { - final aligment = menuService.alignment; + final alignment = menuService.alignment; final offset = menuService.offset; menuService.dismiss(); @@ -41,8 +41,8 @@ void showLinkToPageMenu( _linkToPageMenu?.remove(); _linkToPageMenu = OverlayEntry(builder: (context) { return Positioned( - top: aligment == Alignment.bottomLeft ? offset.dy : null, - bottom: aligment == Alignment.topLeft ? offset.dy : null, + top: alignment == Alignment.bottomLeft ? offset.dy : null, + bottom: alignment == Alignment.topLeft ? offset.dy : null, left: offset.dx, child: Material( color: Colors.transparent, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart index cccb671e7c..c81efbb279 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart @@ -17,7 +17,8 @@ SelectionMenuItem boardMenuItem = SelectionMenuItem( : editorState.editorStyle.selectionMenuItemIconColor, ); }, - keywords: ['board', 'kanban'], + // TODO(a-wallen): Translate keywords + keywords: ['referenced board', 'referenced kanban'], handler: (editorState, menuService, context) { showLinkToPageMenu( editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart new file mode 100644 index 0000000000..b25fe52679 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_view_menu_item.dart @@ -0,0 +1,61 @@ +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/prelude.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart'; +import 'package:appflowy/workspace/application/app/app_service.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flutter/material.dart'; + +SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) => + SelectionMenuItem( + name: LocaleKeys.document_slashMenu_board_createANewBoard.tr(), + icon: (editorState, onSelected) { + return svgWidget( + 'editor/board', + size: const Size.square(18.0), + color: onSelected + ? editorState.editorStyle.selectionMenuItemSelectedIconColor + : editorState.editorStyle.selectionMenuItemIconColor, + ); + }, + // TODO(a-wallen): Translate keywords. + keywords: ['board', 'kanban'], + handler: (editorState, menuService, context) async { + if (!documentBloc.view.hasAppId()) { + return; + } + + final appId = documentBloc.view.appId; + final service = AppBackendService(); + + final result = (await service.createView( + appId: appId, + name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + layoutType: ViewLayoutTypePB.Board, + )) + .getLeftOrNull(); + + // If the result is null, then something went wrong here. + if (result == null) { + return; + } + + final app = + (await service.readApp(appId: result.appId)).getLeftOrNull(); + // We should show an error dialog. + if (app == null) { + return; + } + + final view = + (await service.getView(result.appId, result.id)).getLeftOrNull(); + // As this. + if (view == null) { + return; + } + + editorState.insertPage(app, view); + }, + ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart index 32f402cb83..5e6d59be81 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_image_picker.dart @@ -1,3 +1,4 @@ + import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart'; @@ -25,6 +26,84 @@ class CoverImagePicker extends StatefulWidget { } class _CoverImagePickerState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CoverImagePickerBloc() + ..add(const CoverImagePickerEvent.initialEvent()), + child: BlocListener( + listener: (context, state) { + if (state is NetworkImagePicked) { + state.successOrFail.isRight() + ? showSnapBar(context, + LocaleKeys.document_plugins_cover_invalidImageUrl.tr()) + : null; + } + if (state is Done) { + state.successOrFail.fold( + (l) => widget.onFileSubmit(l), + (r) => showSnapBar( + context, + LocaleKeys.document_plugins_cover_failedToAddImageToGallery + .tr())); + } + }, + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + state is Loading + ? const SizedBox( + height: 180, + child: Center( + child: CircularProgressIndicator(), + ), + ) + : CoverImagePreviewWidget(state: state), + const SizedBox( + height: 10, + ), + NetworkImageUrlInput( + onAdd: (url) { + context.read().add(UrlSubmit(url)); + }, + ), + const SizedBox( + height: 10, + ), + ImagePickerActionButtons( + onBackPressed: () { + widget.onBackPressed(); + }, + onSave: () { + context.read().add( + SaveToGallery(state), + ); + }, + ), + ], + ); + }, + ), + ), + ); + } +} + +class NetworkImageUrlInput extends StatefulWidget { + final void Function(String color) onAdd; + + const NetworkImageUrlInput({ + super.key, + required this.onAdd, + }); + + @override + State createState() => _NetworkImageUrlInputState(); +} + +class _NetworkImageUrlInputState extends State { TextEditingController urlController = TextEditingController(); bool get buttonDisabled => urlController.text.isEmpty; @@ -36,6 +115,85 @@ class _CoverImagePickerState extends State { }); } + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + flex: 4, + child: FlowyTextField( + controller: urlController, + hintText: LocaleKeys.document_plugins_cover_enterImageUrl.tr(), + ), + ), + const SizedBox( + width: 5, + ), + Expanded( + flex: 1, + child: RoundedTextButton( + onPressed: () { + urlController.text.isNotEmpty + ? widget.onAdd(urlController.text) + : null; + }, + hoverColor: Colors.transparent, + fillColor: buttonDisabled + ? Colors.grey + : Theme.of(context).colorScheme.primary, + height: 36, + title: LocaleKeys.document_plugins_cover_add.tr(), + borderRadius: Corners.s8Border, + ), + ) + ], + ); + } +} + +class ImagePickerActionButtons extends StatelessWidget { + final VoidCallback onBackPressed; + final VoidCallback onSave; + + const ImagePickerActionButtons( + {super.key, required this.onBackPressed, required this.onSave}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FlowyTextButton( + LocaleKeys.document_plugins_cover_back.tr(), + hoverColor: Colors.transparent, + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.end, + onPressed: () => onBackPressed(), + ), + FlowyTextButton( + LocaleKeys.document_plugins_cover_saveToGallery.tr(), + onPressed: () => onSave(), + hoverColor: Colors.transparent, + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.end, + fontColor: Theme.of(context).colorScheme.primary, + ), + ], + ); + } +} + +class CoverImagePreviewWidget extends StatefulWidget { + final dynamic state; + + const CoverImagePreviewWidget({super.key, required this.state}); + + @override + State createState() => + _CoverImagePreviewWidgetState(); +} + +class _CoverImagePreviewWidgetState extends State { _buildFilePickerWidget(BuildContext ctx) { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -105,150 +263,43 @@ class _CoverImagePickerState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => CoverImagePickerBloc() - ..add(const CoverImagePickerEvent.initialEvent()), - child: BlocListener( - listener: (context, state) { - if (state is NetworkImagePicked) { - state.successOrFail.isRight() - ? showSnapBar(context, - LocaleKeys.document_plugins_cover_invalidImageUrl.tr()) - : null; - } - if (state is Done) { - state.successOrFail.fold( - (l) => widget.onFileSubmit(l), - (r) => showSnapBar( - context, - LocaleKeys.document_plugins_cover_failedToAddImageToGallery - .tr())); - } - }, - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - state is Loading - ? const SizedBox( - height: 180, - child: Center( - child: CircularProgressIndicator(), + return Stack( + children: [ + Container( + height: 180, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: Corners.s6Border, + image: widget.state is Initial + ? null + : widget.state is NetworkImagePicked + ? widget.state.successOrFail.fold( + (path) => DecorationImage( + image: NetworkImage(path), fit: BoxFit.cover), + (r) => null) + : widget.state is FileImagePicked + ? DecorationImage( + image: FileImage(File(widget.state.path)), + fit: BoxFit.cover) + : null), + child: (widget.state is Initial) + ? _buildFilePickerWidget(context) + : (widget.state is NetworkImagePicked) + ? widget.state.successOrFail.fold( + (l) => null, + (r) => _buildFilePickerWidget( + context, ), ) - : Stack( - children: [ - Container( - height: 180, - alignment: Alignment.center, - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.secondary, - borderRadius: Corners.s6Border, - image: state is Initial - ? null - : state is NetworkImagePicked - ? state.successOrFail.fold( - (path) => DecorationImage( - image: NetworkImage(path), - fit: BoxFit.cover), - (r) => null) - : state is FileImagePicked - ? DecorationImage( - image: FileImage( - File(state.path)), - fit: BoxFit.cover) - : null), - child: (state is Initial) - ? _buildFilePickerWidget(context) - : (state is NetworkImagePicked) - ? state.successOrFail.fold( - (l) => null, - (r) => _buildFilePickerWidget( - context, - ), - ) - : null), - (state is FileImagePicked) - ? _buildImageDeleteButton(context) - : (state is NetworkImagePicked) - ? state.successOrFail.fold( - (l) => _buildImageDeleteButton(context), - (r) => Container()) - : Container() - ], - ), - const SizedBox( - height: 10, - ), - Row( - children: [ - Expanded( - flex: 4, - child: FlowyTextField( - controller: urlController, - hintText: LocaleKeys - .document_plugins_cover_enterImageUrl - .tr(), - ), - ), - const SizedBox( - width: 5, - ), - Expanded( - flex: 1, - child: RoundedTextButton( - onPressed: () { - urlController.text.isNotEmpty - ? context - .read() - .add(UrlSubmit(urlController.text)) - : null; - }, - hoverColor: Colors.transparent, - fillColor: buttonDisabled - ? Colors.grey - : Theme.of(context).colorScheme.primary, - height: 36, - title: LocaleKeys.document_plugins_cover_add.tr(), - borderRadius: Corners.s8Border, - ), - ) - ], - ), - const SizedBox( - height: 10, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FlowyTextButton( - LocaleKeys.document_plugins_cover_back.tr(), - hoverColor: Colors.transparent, - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.end, - onPressed: () => widget.onBackPressed(), - ), - FlowyTextButton( - LocaleKeys.document_plugins_cover_saveToGallery.tr(), - onPressed: () async { - context - .read() - .add(SaveToGallery(state)); - }, - hoverColor: Colors.transparent, - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.end, - fontColor: Theme.of(context).colorScheme.primary, - ), - ], - ) - ], - ); - }, - ), - ), + : null), + (widget.state is FileImagePicked) + ? _buildImageDeleteButton(context) + : (widget.state is NetworkImagePicked) + ? widget.state.successOrFail.fold( + (l) => _buildImageDeleteButton(context), (r) => Container()) + : Container() + ], ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart index 67b083a774..808962e019 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart @@ -463,7 +463,7 @@ class _CoverImageState extends State<_CoverImage> { coverImage = const SizedBox(); break; } -//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr +//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error return SizedBox( height: height, child: OverflowBox( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart index 737c17f4f5..b2775543e9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart @@ -17,7 +17,7 @@ SelectionMenuItem gridMenuItem = SelectionMenuItem( : editorState.editorStyle.selectionMenuItemIconColor, ); }, - keywords: ['grid'], + keywords: ['referenced grid'], handler: (editorState, menuService, context) { showLinkToPageMenu( editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart new file mode 100644 index 0000000000..2dc030d547 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_view_menu_item.dart @@ -0,0 +1,60 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/doc_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/base/insert_page_command.dart'; +import 'package:appflowy/workspace/application/app/app_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flutter/material.dart'; + +SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) => + SelectionMenuItem( + name: LocaleKeys.document_slashMenu_grid_createANewGrid.tr(), + icon: (editorState, onSelected) { + return svgWidget( + 'editor/grid', + size: const Size.square(18.0), + color: onSelected + ? editorState.editorStyle.selectionMenuItemSelectedIconColor + : editorState.editorStyle.selectionMenuItemIconColor, + ); + }, + keywords: ['grid'], + handler: (editorState, menuService, context) async { + if (!documentBloc.view.hasAppId()) { + return; + } + + final appId = documentBloc.view.appId; + final service = AppBackendService(); + + final result = (await service.createView( + appId: appId, + name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + layoutType: ViewLayoutTypePB.Grid, + )) + .getLeftOrNull(); + + // If the result is null, then something went wrong here. + if (result == null) { + return; + } + + final app = + (await service.readApp(appId: result.appId)).getLeftOrNull(); + // We should show an error dialog. + if (app == null) { + return; + } + + final view = + (await service.getView(result.appId, result.id)).getLeftOrNull(); + // As this. + if (view == null) { + return; + } + + editorState.insertPage(app, view); + }, + ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart index 24c664abe4..ccb4b08866 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'text_completion.dart'; import 'package:dartz/dartz.dart'; @@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository { String? suffix, int maxTokens = 2048, double temperature = 0.3, + bool useAction = false, }) async { final parameters = { 'model': 'text-davinci-003', @@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository { .transform(const Utf8Decoder()) .transform(const LineSplitter())) { syntax += 1; - if (syntax == 3) { - await onStart(); - continue; - } else if (syntax < 3) { - continue; + if (!useAction) { + if (syntax == 3) { + await onStart(); + continue; + } else if (syntax < 3) { + continue; + } + } else { + if (syntax == 2) { + await onStart(); + continue; + } else if (syntax < 2) { + continue; + } } final data = chunk.trim().split('data: '); - Log.editor.info(data.toString()); if (data.length > 1) { if (data[1] != '[DONE]') { final response = TextCompletionResponse.fromJson( @@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository { previousSyntax = response.choices.first.text; } } else { - onEnd(); + await onEnd(); } } } @@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository { OpenAIError.fromJson(json.decode(body)['error']), ); } + return; } @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart new file mode 100644 index 0000000000..abdeeb162e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/learn_more_action.dart @@ -0,0 +1,9 @@ +import 'package:url_launcher/url_launcher.dart'; + +Future openLearnMorePage() async { + final uri = Uri.parse( + 'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart index c5d7a39946..046f99f65a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -9,7 +11,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/rendering.dart'; import 'package:http/http.dart' as http; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -56,6 +58,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { final controller = TextEditingController(); final focusNode = FocusNode(); final textFieldFocusNode = FocusNode(); + final interceptor = SelectionInterceptor(); @override void initState() { @@ -63,6 +66,34 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { textFieldFocusNode.addListener(_onFocusChanged); textFieldFocusNode.requestFocus(); + widget.editorState.service.selectionService.register(interceptor + ..canTap = (details) { + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox != null) { + if (!isTapDownDetailsInRenderBox(details, renderBox)) { + if (text.isNotEmpty || controller.text.isNotEmpty) { + showDialog( + context: context, + builder: (context) { + return DiscardDialog( + onConfirm: () => _onDiscard(), + onCancel: () {}, + ); + }, + ); + } else if (controller.text.isEmpty) { + _onExit(); + } + } + } + return false; + }); + } + + bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) { + var result = BoxHitTestResult(); + box.hitTest(result, position: box.globalToLocal(details.globalPosition)); + return result.path.any((entry) => entry.target == box); } @override @@ -71,6 +102,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { textFieldFocusNode.removeListener(_onFocusChanged); widget.editorState.service.selectionService.currentSelection .removeListener(_onCancelWhenSelectionChanged); + widget.editorState.service.selectionService.unRegister(interceptor); super.dispose(); } @@ -119,34 +151,26 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { fontSize: 14, ), const Spacer(), - FlowyText.regular( - LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), - ), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText.regular( + LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), + ), + onTap: () async { + await openLearnMorePage(); + }, + ) ], ); } Widget _buildInputWidget(BuildContext context) { - return RawKeyboardListener( - focusNode: focusNode, - onKey: (RawKeyEvent event) async { - if (event is! RawKeyDownEvent) return; - if (event.logicalKey == LogicalKeyboardKey.enter) { - if (controller.text.isNotEmpty) { - textFieldFocusNode.unfocus(); - await _onGenerate(); - } - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - await _onExit(); - } - }, - child: FlowyTextField( - hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(), - controller: controller, - maxLines: 3, - focusNode: textFieldFocusNode, - autoFocus: false, - ), + return FlowyTextField( + hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(), + controller: controller, + maxLines: 3, + focusNode: textFieldFocusNode, + autoFocus: false, ); } @@ -157,15 +181,9 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { TextSpan( children: [ TextSpan( - text: '${LocaleKeys.button_generate.tr()} ', + text: LocaleKeys.button_generate.tr(), style: Theme.of(context).textTheme.bodyMedium, ), - TextSpan( - text: '↵', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), ], ), onPressed: () async => await _onGenerate(), @@ -175,19 +193,23 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { TextSpan( children: [ TextSpan( - text: '${LocaleKeys.button_Cancel.tr()} ', + text: LocaleKeys.button_Cancel.tr(), style: Theme.of(context).textTheme.bodyMedium, ), - TextSpan( - text: LocaleKeys.button_esc.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), ], ), onPressed: () async => await _onExit(), ), + Expanded( + child: Container( + alignment: Alignment.centerRight, + child: FlowyText.regular( + LocaleKeys.document_plugins_warning.tr(), + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart index 88ba17b47d..ce9eb5dbef 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart @@ -2,10 +2,13 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node( - name: 'Auto Generator', + name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(), iconData: Icons.generating_tokens, - keywords: ['autogenerator', 'auto generator'], + keywords: ['ai', 'openai' 'writer', 'autogenerator'], nodeBuilder: (editorState) { final node = Node( type: kAutoCompletionInputType, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart new file mode 100644 index 0000000000..b2f314c425 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart @@ -0,0 +1,28 @@ +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; + +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +import 'package:easy_localization/easy_localization.dart'; + +class DiscardDialog extends StatelessWidget { + const DiscardDialog({ + super.key, + required this.onConfirm, + required this.onCancel, + }); + + final VoidCallback onConfirm; + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + return NavigatorOkCancelDialog( + message: LocaleKeys.document_plugins_discardResponse.tr(), + okTitle: LocaleKeys.button_discard.tr(), + cancelTitle: LocaleKeys.button_Cancel.tr(), + onOkPressed: onConfirm, + onCancelPressed: onCancel, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart index e34fe1e8d6..a2c5ce97e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/loading.dart @@ -17,7 +17,7 @@ class Loading { return const SimpleDialog( elevation: 0.0, backgroundColor: - Colors.transparent, // can change this to your prefered color + Colors.transparent, // can change this to your preferred color children: [ Center( child: CircularProgressIndicator(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart index 21678c16e5..053aaaa739 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart @@ -10,11 +10,39 @@ enum SmartEditAction { String get toInstruction { switch (this) { case SmartEditAction.summarize: - return 'Make this shorter and more concise:'; + return 'Tl;dr'; case SmartEditAction.fixSpelling: return 'Correct this to standard English:'; } } + + String prompt(String input) { + switch (this) { + case SmartEditAction.summarize: + return '$input\n\nTl;dr'; + case SmartEditAction.fixSpelling: + return 'Correct this to standard English:\n\n$input'; + } + } + + static SmartEditAction from(int index) { + switch (index) { + case 0: + return SmartEditAction.summarize; + case 1: + return SmartEditAction.fixSpelling; + } + return SmartEditAction.fixSpelling; + } + + String get name { + switch (this) { + case SmartEditAction.summarize: + return LocaleKeys.document_plugins_smartEditSummarize.tr(); + case SmartEditAction.fixSpelling: + return LocaleKeys.document_plugins_smartEditFixSpelling.tr(); + } + } } class SmartEditActionWrapper extends ActionCell { @@ -26,11 +54,6 @@ class SmartEditActionWrapper extends ActionCell { @override String get name { - switch (inner) { - case SmartEditAction.summarize: - return LocaleKeys.document_plugins_smartEditSummarize.tr(); - case SmartEditAction.fixSpelling: - return LocaleKeys.document_plugins_smartEditFixSpelling.tr(); - } + return inner.name; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart index 0d9c9c29fa..151ccc60d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart @@ -1,19 +1,18 @@ -import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart'; +import 'dart:async'; + import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart'; -import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart'; +import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:http/http.dart' as http; -import 'package:dartz/dartz.dart' as dartz; -import 'package:appflowy/util/either_extension.dart'; const String kSmartEditType = 'smart_edit_input'; const String kSmartEditInstructionType = 'smart_edit_instruction'; @@ -22,15 +21,15 @@ const String kSmartEditInputType = 'smart_edit_input'; class SmartEditInputBuilder extends NodeWidgetBuilder { @override NodeValidator get nodeValidator => (node) { - return SmartEditAction.values.map((e) => e.toInstruction).contains( - node.attributes[kSmartEditInstructionType], - ) && + return SmartEditAction.values + .map((e) => e.index) + .contains(node.attributes[kSmartEditInstructionType]) && node.attributes[kSmartEditInputType] is String; }; @override Widget build(NodeWidgetContext context) { - return _SmartEditInput( + return _HoverSmartInput( key: context.node.key, node: context.node, editorState: context.editorState, @@ -38,28 +37,111 @@ class SmartEditInputBuilder extends NodeWidgetBuilder { } } -class _SmartEditInput extends StatefulWidget { - final Node node; - - final EditorState editorState; - const _SmartEditInput({ - Key? key, +class _HoverSmartInput extends StatefulWidget { + const _HoverSmartInput({ + required super.key, required this.node, required this.editorState, }); + final Node node; + final EditorState editorState; + + @override + State<_HoverSmartInput> createState() => _HoverSmartInputState(); +} + +class _HoverSmartInputState extends State<_HoverSmartInput> { + final popoverController = PopoverController(); + final key = GlobalKey(debugLabel: 'smart_edit_input'); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + popoverController.show(); + }); + } + + @override + Widget build(BuildContext context) { + final width = _maxWidth(); + + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + constraints: BoxConstraints(maxWidth: width), + decoration: FlowyDecoration.decoration( + Colors.transparent, + Colors.transparent, + ), + child: const SizedBox( + width: double.infinity, + ), + canClose: () async { + final completer = Completer(); + final state = key.currentState as _SmartEditInputState; + if (state.result.isEmpty) { + completer.complete(true); + } else { + showDialog( + context: context, + builder: (context) { + return DiscardDialog( + onConfirm: () => completer.complete(true), + onCancel: () => completer.complete(false), + ); + }, + ); + } + return completer.future; + }, + popupBuilder: (BuildContext popoverContext) { + return _SmartEditInput( + key: key, + node: widget.node, + editorState: widget.editorState, + ); + }, + ); + } + + double _maxWidth() { + var width = double.infinity; + final editorSize = widget.editorState.renderBox?.size; + final padding = widget.editorState.editorStyle.padding; + if (editorSize != null && padding != null) { + width = editorSize.width - padding.left - padding.right; + } + return width; + } +} + +class _SmartEditInput extends StatefulWidget { + const _SmartEditInput({ + required super.key, + required this.node, + required this.editorState, + }); + + final Node node; + final EditorState editorState; + @override State<_SmartEditInput> createState() => _SmartEditInputState(); } class _SmartEditInputState extends State<_SmartEditInput> { - String get instruction => widget.node.attributes[kSmartEditInstructionType]; + SmartEditAction get action => + SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]); String get input => widget.node.attributes[kSmartEditInputType]; final focusNode = FocusNode(); final client = http.Client(); - dartz.Either? result; bool loading = true; + String result = ''; @override void initState() { @@ -72,12 +154,7 @@ class _SmartEditInputState extends State<_SmartEditInput> { widget.editorState.service.keyboardService?.enable(); } }); - _requestEdits().then( - (value) => setState(() { - result = value; - loading = false; - }), - ); + _requestCompletions(); } @override @@ -99,28 +176,16 @@ class _SmartEditInputState extends State<_SmartEditInput> { } Widget _buildSmartEditPanel(BuildContext context) { - return RawKeyboardListener( - focusNode: focusNode, - onKey: (RawKeyEvent event) async { - if (event is! RawKeyDownEvent) return; - if (event.logicalKey == LogicalKeyboardKey.enter) { - await _onReplace(); - await _onExit(); - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - await _onExit(); - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeaderWidget(context), - const Space(0, 10), - _buildResultWidget(context), - const Space(0, 10), - _buildInputFooterWidget(context), - ], - ), + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderWidget(context), + const Space(0, 10), + _buildResultWidget(context), + const Space(0, 10), + _buildInputFooterWidget(context), + ], ); } @@ -128,13 +193,19 @@ class _SmartEditInputState extends State<_SmartEditInput> { return Row( children: [ FlowyText.medium( - LocaleKeys.document_plugins_smartEditTitleName.tr(), + '${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}', fontSize: 14, ), const Spacer(), - FlowyText.regular( - LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), - ), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText.regular( + LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), + ), + onTap: () async { + await openLearnMorePage(); + }, + ) ], ); } @@ -147,25 +218,14 @@ class _SmartEditInputState extends State<_SmartEditInput> { child: const CircularProgressIndicator(), ), ); - if (result == null) { + if (result.isEmpty) { return loading; } - return result!.fold((error) { - return Flexible( - child: Text( - error.message, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.red, - ), - ), - ); - }, (response) { - return Flexible( - child: Text( - response.choices.map((e) => e.text).join('\n'), - ), - ); - }); + return Flexible( + child: Text( + result, + ), + ); } Widget _buildInputFooterWidget(BuildContext context) { @@ -175,19 +235,13 @@ class _SmartEditInputState extends State<_SmartEditInput> { TextSpan( children: [ TextSpan( - text: '${LocaleKeys.button_replace.tr()} ', + text: LocaleKeys.button_replace.tr(), style: Theme.of(context).textTheme.bodyMedium, ), - TextSpan( - text: '↵', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), ], ), - onPressed: () { - _onReplace(); + onPressed: () async { + await _onReplace(); _onExit(); }, ), @@ -196,19 +250,33 @@ class _SmartEditInputState extends State<_SmartEditInput> { TextSpan( children: [ TextSpan( - text: '${LocaleKeys.button_Cancel.tr()} ', + text: LocaleKeys.button_insertBelow.tr(), style: Theme.of(context).textTheme.bodyMedium, ), + ], + ), + onPressed: () async { + await _onInsertBelow(); + _onExit(); + }, + ), + const Space(10, 0), + FlowyRichTextButton( + TextSpan( + children: [ TextSpan( - text: LocaleKeys.button_esc.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), + text: LocaleKeys.button_Cancel.tr(), + style: Theme.of(context).textTheme.bodyMedium, ), ], ), onPressed: () async => await _onExit(), ), + const Spacer(), + FlowyText.regular( + LocaleKeys.document_plugins_warning.tr(), + color: Theme.of(context).hintColor, + ), ], ); } @@ -219,12 +287,11 @@ class _SmartEditInputState extends State<_SmartEditInput> { final selectedNodes = widget .editorState.service.selectionService.currentSelectedNodes.normalized .whereType(); - if (selection == null || result == null || result!.isLeft()) { + if (selection == null || result.isEmpty) { return; } - final texts = result!.asRight().choices.first.text.split('\n') - ..removeWhere((element) => element.isEmpty); + final texts = result.split('\n')..removeWhere((element) => element.isEmpty); final transaction = widget.editorState.transaction; transaction.replaceTexts( selectedNodes.toList(growable: false), @@ -234,6 +301,25 @@ class _SmartEditInputState extends State<_SmartEditInput> { return widget.editorState.apply(transaction); } + Future _onInsertBelow() async { + final selection = widget.editorState.service.selectionService + .currentSelection.value?.normalized; + if (selection == null || result.isEmpty) { + return; + } + final texts = result.split('\n')..removeWhere((element) => element.isEmpty); + final transaction = widget.editorState.transaction; + transaction.insertNodes( + selection.normalized.end.path.next, + texts.map( + (e) => TextNode( + delta: Delta()..insert(e), + ), + ), + ); + return widget.editorState.apply(transaction); + } + Future _onExit() async { final transaction = widget.editorState.transaction; transaction.deleteNode(widget.node); @@ -246,35 +332,63 @@ class _SmartEditInputState extends State<_SmartEditInput> { ); } - Future> _requestEdits() async { + Future _requestCompletions() async { final result = await UserBackendService.getCurrentUserProfile(); - return result.fold((userProfile) async { + return result.fold((l) async { final openAIRepository = HttpOpenAIRepository( client: client, - apiKey: userProfile.openaiKey, + apiKey: l.openaiKey, ); - final edits = await openAIRepository.getEdits( - input: input, - instruction: instruction, - n: 1, - ); - return edits.fold((error) async { - return dartz.Left( - OpenAIError( - message: - LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(), - ), + + var lines = input.split('\n\n'); + if (action == SmartEditAction.summarize) { + lines = [lines.join('\n')]; + } + for (var i = 0; i < lines.length; i++) { + final element = lines[i]; + await openAIRepository.getStreamedCompletions( + useAction: true, + prompt: action.prompt(element), + onStart: () async { + setState(() { + loading = false; + }); + }, + onProcess: (response) async { + setState(() { + this.result += response.choices.first.text; + }); + }, + onEnd: () async { + setState(() { + if (i != lines.length - 1) { + this.result += '\n'; + } + }); + }, + onError: (error) async { + await _showError(error.message); + await _onExit(); + }, ); - }, (textEdit) async { - return dartz.Right(textEdit); - }); - }, (error) async { - // error - return dartz.Left( - OpenAIError( - message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(), - ), - ); + } + }, (r) async { + await _showError(r.msg); + await _onExit(); }); } + + Future _showError(String message) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + action: SnackBarAction( + label: LocaleKeys.button_Cancel.tr(), + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + content: FlowyText(message), + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart index 844ea8df91..8db094e1dc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart @@ -101,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> { textNodes.normalized, selection.normalized, ); + while (input.last.isEmpty) { + input.removeLast(); + } final transaction = widget.editorState.transaction; transaction.insertNode( selection.normalized.end.path.next, Node( type: kSmartEditType, attributes: { - kSmartEditInstructionType: actionWrapper.inner.toInstruction, - kSmartEditInputType: input, + kSmartEditInstructionType: actionWrapper.inner.index, + kSmartEditInputType: input.join('\n\n'), }, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart new file mode 100644 index 0000000000..88ec444dec --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart @@ -0,0 +1,13 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +class CodeBlockNodeParser extends NodeParser { + const CodeBlockNodeParser(); + + @override + String get id => 'code_block'; + + @override + String transform(Node node) { + return '```\n${node.attributes['code_block']}\n```'; + } +} diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index ebe027939d..de2d612250 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -27,7 +27,7 @@ import 'tasks/prelude.dart'; // └───▶│AppWidgetTask│────────▶│ApplicationWidget │─────▶│ SplashScreen │ // └─────────────┘ └──────────────────┘ └───────────────┘ // -// 3.build MeterialApp +// 3.build MaterialApp final getIt = GetIt.instance; abstract class EntryPoint { diff --git a/frontend/appflowy_flutter/lib/util/debounce.dart b/frontend/appflowy_flutter/lib/util/debounce.dart new file mode 100644 index 0000000000..324818a650 --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/debounce.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class Debounce { + final Duration duration; + Timer? _timer; + + Debounce({ + this.duration = const Duration(milliseconds: 1000), + }); + + void call(VoidCallback action) { + dispose(); + _timer = Timer(duration, () { + action(); + }); + } + + void dispose() { + _timer?.cancel(); + _timer = null; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart index 2515750718..df9d203974 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart @@ -141,7 +141,7 @@ class AppEvent with _$AppEvent { PluginBuilder pluginBuilder, { String? desc, - /// The initial data should be the JSON of the doucment + /// The initial data should be the JSON of the document /// For example: {"document":{"type":"editor","children":[]}} String? initialData, Map? ext, diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart index b5c3ecb9bf..9cadf7f5ce 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart @@ -55,9 +55,9 @@ class AppearanceSettingsCubit extends Cubit { newLocale = const Locale('en'); } - context.setLocale(newLocale); - if (state.locale != newLocale) { + context.setLocale(newLocale); + _setting.locale.languageCode = newLocale.languageCode; _setting.locale.countryCode = newLocale.countryCode ?? ""; _saveAppearanceSettings(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart index e91d046ded..b16d9b0e57 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart @@ -33,14 +33,14 @@ class ViewSection extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - return _reorderableColum(context, state); + return _reorderableColumn(context, state); }, ), ), ); } - ReorderableColumn _reorderableColum( + ReorderableColumn _reorderableColumn( BuildContext context, ViewSectionState state) { final children = state.views.map((view) { return ViewSectionItem( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 6f89f77a37..d7b3f35a17 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -12,6 +12,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12); +const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0); + class SettingsDialog extends StatelessWidget { final UserProfilePB user; SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id)); @@ -23,34 +26,46 @@ class SettingsDialog extends StatelessWidget { ..add(const SettingsDialogEvent.initial()), child: BlocBuilder( builder: (context, state) => FlowyDialog( - title: FlowyText( - LocaleKeys.settings_title.tr(), - fontSize: 20, - fontWeight: FontWeight.w700, + title: Padding( + padding: _dialogHorizontalPadding + _contentInsetPadding, + child: FlowyText( + LocaleKeys.settings_title.tr(), + fontSize: 20, + fontWeight: FontWeight.w700, + ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: SettingsMenu( - changeSelectedPage: (index) { - context - .read() - .add(SettingsDialogEvent.setSelectedPage(index)); - }, - currentPage: context.read().state.page, + child: ScaffoldMessenger( + child: Scaffold( + backgroundColor: Colors.transparent, + body: Padding( + padding: _dialogHorizontalPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: SettingsMenu( + changeSelectedPage: (index) { + context + .read() + .add(SettingsDialogEvent.setSelectedPage(index)); + }, + currentPage: + context.read().state.page, + ), + ), + const VerticalDivider(), + const SizedBox(width: 10), + Expanded( + child: getSettingsView( + context.read().state.page, + context.read().state.userProfile, + ), + ) + ], ), ), - const VerticalDivider(), - const SizedBox(width: 10), - Expanded( - child: getSettingsView( - context.read().state.page, - context.read().state.userProfile, - ), - ) - ], + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart index 713e120092..8390b2a8af 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -46,7 +46,17 @@ class SettingsFileLocationCustomzierState onDoubleTap: () { Clipboard.setData(ClipboardData( text: state.path, - )); + )).then((_) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: FlowyText( + LocaleKeys.settings_files_pathCopiedSnackbar.tr(), + ), + ), + ); + } + }); }, child: FlowyText.regular( state.path ?? '', diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart index ffb16a2f41..7b76aa6157 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -54,38 +54,37 @@ class _LanguageSelectorDropdownState extends State { @override Widget build(BuildContext context) { return MouseRegion( - onEnter: (event) => {hoverEnterLanguage()}, - onExit: (event) => {hoverExitLanguage()}, + onEnter: (_) => hoverEnterLanguage(), + onExit: (_) => hoverExitLanguage(), child: Container( - margin: const EdgeInsets.only(left: 8, right: 8), + margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: currHoverColor, ), child: DropdownButtonHideUnderline( - child: DropdownButton( - value: context.locale, - onChanged: (val) { - setState(() { + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: DropdownButton( + value: context.locale, + onChanged: (locale) { context .read() - .setLocale(context, val!); - }); - }, - icon: const Visibility( - visible: false, - child: (Icon(Icons.arrow_downward)), + .setLocale(context, locale!); + }, + autofocus: true, + borderRadius: BorderRadius.circular(8), + items: + EasyLocalization.of(context)!.supportedLocales.map((locale) { + return DropdownMenuItem( + value: locale, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: FlowyText.medium(languageFromLocale(locale)), + ), + ); + }).toList(), ), - borderRadius: BorderRadius.circular(8), - items: EasyLocalization.of(context)!.supportedLocales.map((locale) { - return DropdownMenuItem( - value: locale, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: FlowyText.medium(languageFromLocale(locale)), - ), - ); - }).toList(), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index 35e786ff38..1b6f7a9a6f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -1,4 +1,5 @@ import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/debounce.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -98,11 +99,20 @@ class _OpenaiKeyInput extends StatefulWidget { class _OpenaiKeyInputState extends State<_OpenaiKeyInput> { bool visible = false; + final textEditingController = TextEditingController(); + final debounce = Debounce(); + + @override + void initState() { + super.initState(); + + textEditingController.text = widget.openAIKey; + } @override Widget build(BuildContext context) { return TextField( - controller: TextEditingController()..text = widget.openAIKey, + controller: textEditingController, obscureText: !visible, decoration: InputDecoration( labelText: 'OpenAI Key', @@ -120,13 +130,21 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> { }, ), ), - onSubmitted: (val) { - context - .read() - .add(SettingsUserEvent.updateUserOpenAIKey(val)); + onChanged: (value) { + debounce.call(() { + context + .read() + .add(SettingsUserEvent.updateUserOpenAIKey(value)); + }); }, ); } + + @override + void dispose() { + debounce.dispose(); + super.dispose(); + } } class _CurrentIcon extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index bce35021ef..c6bb7f3ca0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -135,7 +135,6 @@ class FlowyVersionDescription extends CustomActionCell { PackageInfo packageInfo = snapshot.data; String appName = packageInfo.appName; String version = packageInfo.version; - String buildNumber = packageInfo.buildNumber; return SizedBox( height: 30, @@ -149,7 +148,7 @@ class FlowyVersionDescription extends CustomActionCell { thickness: 1.0), const VSpace(6), FlowyText( - "$appName $version.$buildNumber", + "$appName $version", color: Theme.of(context).hintColor, ), ], diff --git a/frontend/appflowy_flutter/linux/CMakeLists.txt b/frontend/appflowy_flutter/linux/CMakeLists.txt index 26146072f2..3c6927375c 100644 --- a/frontend/appflowy_flutter/linux/CMakeLists.txt +++ b/frontend/appflowy_flutter/linux/CMakeLists.txt @@ -1,8 +1,8 @@ cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) -set(BINARY_NAME "appflowy_flutter") -set(APPLICATION_ID "com.example.appflowy_flutter") +set(BINARY_NAME "AppFlowy") +set(APPLICATION_ID "io.appflowy.appflowy") cmake_policy(SET CMP0063 NEW) diff --git a/frontend/appflowy_flutter/linux/appflowy.desktop.temp b/frontend/appflowy_flutter/linux/appflowy.desktop.temp index d23fdec42b..2b189ef243 100644 --- a/frontend/appflowy_flutter/linux/appflowy.desktop.temp +++ b/frontend/appflowy_flutter/linux/appflowy.desktop.temp @@ -2,7 +2,7 @@ Name=AppFlowy Comment=An Open Source Alternative to Notion Icon=[CHANGE_THIS]/AppFlowy/flowy_logo.svg -Exec=[CHANGE_THIS]/AppFlowy/appflowy_flutter +Exec=[CHANGE_THIS]/AppFlowy/AppFlowy Categories=Office Type=Application Terminal=false \ No newline at end of file diff --git a/frontend/appflowy_flutter/linux/my_application.cc b/frontend/appflowy_flutter/linux/my_application.cc index d262d685a7..490ead4cd0 100644 --- a/frontend/appflowy_flutter/linux/my_application.cc +++ b/frontend/appflowy_flutter/linux/my_application.cc @@ -7,17 +7,19 @@ #include "flutter/generated_plugin_registrant.h" -struct _MyApplication { +struct _MyApplication +{ GtkApplication parent_instance; - char** dart_entrypoint_arguments; + char **dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = +static void my_application_activate(GApplication *application) +{ + MyApplication *self = MY_APPLICATION(application); + GtkWindow *window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used @@ -29,22 +31,27 @@ static void my_application_activate(GApplication* application) { // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + GdkScreen *screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) + { + const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) + { use_header_bar = FALSE; } } #endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + if (use_header_bar) + { + GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "appflowy_flutter"); + gtk_header_bar_set_title(header_bar, "AppFlowy"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "appflowy_flutter"); + } + else + { + gtk_window_set_title(window, "AppFlowy"); } gtk_window_set_default_size(window, 1280, 720); @@ -53,7 +60,7 @@ static void my_application_activate(GApplication* application) { g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - FlView* view = fl_view_new(project); + FlView *view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); @@ -63,16 +70,18 @@ static void my_application_activate(GApplication* application) { } // Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); +static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status) +{ + MyApplication *self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; + if (!g_application_register(application, nullptr, &error)) + { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); @@ -82,21 +91,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch } // Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); +static void my_application_dispose(GObject *object) +{ + MyApplication *self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } -static void my_application_class_init(MyApplicationClass* klass) { +static void my_application_class_init(MyApplicationClass *klass) +{ G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } -static void my_application_init(MyApplication* self) {} +static void my_application_init(MyApplication *self) {} -MyApplication* my_application_new() { +MyApplication *my_application_new() +{ return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, diff --git a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig index d0b1c2acf9..656857119f 100644 --- a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = appflowy_flutter +PRODUCT_NAME = AppFlowy // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy +PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2023 AppFlowy.IO. All rights reserved. diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md b/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md index 30926b3d50..89a6eecf9c 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md +++ b/frontend/appflowy_flutter/packages/appflowy_editor/documentation/customizing.md @@ -365,7 +365,7 @@ ThemeData customizeEditorTheme(BuildContext context) { return Theme.of(context).copyWith(extensions: [ editorStyle, - ...darkPlguinStyleExtension, + ...darkPluginStyleExtension, quote, ]); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/documentation/testing.md b/frontend/appflowy_flutter/packages/appflowy_editor/documentation/testing.md index 7e356d9f83..b388edddad 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/documentation/testing.md +++ b/frontend/appflowy_flutter/packages/appflowy_editor/documentation/testing.md @@ -77,10 +77,10 @@ Next we will simulate the input of a shortcut key being pressed that will select ```dart // Meta + A. -await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); +await editor.pressLogicKey(key: LogicalKeyboardKey.keyA, isMetaPressed: true); // Meta + shift + S. -await editor.pressLogicKey( - LogicalKeyboardKey.keyS, +await editor.pressLogicKey + key: LogicalKeyboardKey.keyS, isMetaPressed: true, isShiftPressed: true, ); @@ -130,7 +130,7 @@ void main() async { editor.insertTextNode(text); } await editor.startTesting(); - await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyA, isMetaPressed: true); expect( editor.documentSelection, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart index 89a1c393af..293d7a6445 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/home_page.dart @@ -48,7 +48,7 @@ class _HomePageState extends State { ThemeData _themeData = ThemeData.light().copyWith( extensions: [ ...lightEditorStyleExtension, - ...lightPlguinStyleExtension, + ...lightPluginStyleExtension, ], ); @@ -151,7 +151,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC // Theme Demo _buildSeparator(context, 'Theme Demo'), - _buildListTile(context, 'Bulit In Dark Mode', () { + _buildListTile(context, 'Built In Dark Mode', () { _jsonString = Future.value( jsonEncode(_editorState.document.toJson()).toString(), ); @@ -159,7 +159,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC _themeData = ThemeData.dark().copyWith( extensions: [ ...darkEditorStyleExtension, - ...darkPlguinStyleExtension, + ...darkPluginStyleExtension, ], ); }); @@ -372,7 +372,7 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC return Theme.of(context).copyWith(extensions: [ editorStyle, - ...darkPlguinStyleExtension, + ...darkPluginStyleExtension, quote, ]); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart index 65a8f868fa..be84ae38f9 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/editor_theme.dart @@ -34,7 +34,7 @@ ThemeData customizeEditorTheme(BuildContext context) { return Theme.of(context).copyWith(extensions: [ editorStyle, - ...darkPlguinStyleExtension, + ...darkPluginStyleExtension, quote, ]); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart index bea819e232..44595fcdff 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/appflowy_editor.dart @@ -31,6 +31,7 @@ export 'src/extensions/attributes_extension.dart'; export 'src/render/rich_text/default_selectable.dart'; export 'src/render/rich_text/flowy_rich_text.dart'; export 'src/render/selection_menu/selection_menu_widget.dart'; +export 'src/render/selection_menu/selection_menu_item_widget.dart'; export 'src/l10n/l10n.dart'; export 'src/render/style/plugin_styles.dart'; export 'src/render/style/editor_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart index 8a9648f6a5..71c3ef4a9d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart @@ -52,7 +52,7 @@ extension CommandExtension on EditorState { throw Exception('path and textNode cannot be null at the same time'); } - String getTextInSelection( + List getTextInSelection( List textNodes, Selection selection, ) { @@ -77,6 +77,6 @@ extension CommandExtension on EditorState { } } } - return res.join('\n'); + return res; } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart index c1a311d648..6c369f566e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart @@ -264,11 +264,11 @@ extension TextTransaction on Transaction { if (index != 0 && attributes == null) { newAttributes = textNode.delta.slice(max(index - 1, 0), index).first.attributes; - if (newAttributes != null) { - newAttributes = {...newAttributes}; // make a copy - } else { - newAttributes = - textNode.delta.slice(index, index + length).first.attributes; + if (newAttributes == null) { + final slicedDelta = textNode.delta.slice(index, index + length); + if (slicedDelta.isNotEmpty) { + newAttributes = slicedDelta.first.attributes; + } } } updateText( @@ -276,7 +276,7 @@ extension TextTransaction on Transaction { Delta() ..retain(index) ..delete(length) - ..insert(text, attributes: newAttributes), + ..insert(text, attributes: {...newAttributes ?? {}}), ); afterSelection = Selection.collapsed( Position( @@ -347,23 +347,34 @@ extension TextTransaction on Transaction { textNode.toPlainText().length, texts.first, ); - } else if (i == length - 1) { + } else if (i == length - 1 && texts.length >= 2) { replaceText( textNode, 0, selection.endIndex, texts.last, ); + } else if (i < texts.length - 1) { + replaceText( + textNode, + 0, + textNode.toPlainText().length, + texts[i], + ); } else { - if (i < texts.length - 1) { + deleteNode(textNode); + if (i == textNodes.length - 1) { + final delta = Delta() + ..insert(texts[0]) + ..addAll( + textNodes.last.delta.slice(selection.end.offset), + ); replaceText( textNode, - 0, - textNode.toPlainText().length, - texts[i], + selection.start.offset, + texts[0].length, + delta.toPlainText(), ); - } else { - deleteNode(textNode); } } } @@ -373,6 +384,8 @@ extension TextTransaction on Transaction { if (textNodes.length < texts.length) { final length = texts.length; + var path = textNodes.first.path; + for (var i = 0; i < texts.length; i++) { final text = texts[i]; if (i == 0) { @@ -382,13 +395,15 @@ extension TextTransaction on Transaction { textNodes.first.toPlainText().length, text, ); - } else if (i == length - 1) { + path = path.next; + } else if (i == length - 1 && textNodes.length >= 2) { replaceText( textNodes.last, 0, selection.endIndex, text, ); + path = path.next; } else { if (i < textNodes.length - 1) { replaceText( @@ -397,14 +412,28 @@ extension TextTransaction on Transaction { textNodes[i].toPlainText().length, text, ); + path = path.next; } else { - var path = textNodes.first.path; - var j = i - textNodes.length + length - 1; - while (j > 0) { - path = path.next; - j--; + if (i == texts.length - 1) { + final delta = Delta() + ..insert(text) + ..addAll( + textNodes.last.delta.slice(selection.end.offset), + ); + insertNode( + path, + TextNode( + delta: delta, + ), + ); + } else { + insertNode( + path, + TextNode( + delta: Delta()..insert(text), + ), + ); } - insertNode(path, TextNode(delta: Delta()..insert(text))); } } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart index 44e4e3d5f5..4b214adad6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/editor_state.dart @@ -77,7 +77,7 @@ class EditorState { // TODO: only for testing. bool disableSealTimer = false; - bool disbaleRules = false; + bool disableRules = false; bool editable = true; @@ -209,7 +209,7 @@ class EditorState { void _applyRules(int ruleCount) { // Set a maximum count to prevent a dead loop. - if (ruleCount >= 5 || disbaleRules) { + if (ruleCount >= 5 || disableRules) { return; } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 7e0ce3eeea..2d5af7bae1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -69,7 +69,7 @@ class SelectionMenu implements SelectionMenuService { editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; final editorHeight = editorState.renderBox!.size.height; - // show below defualt + // show below default var showBelow = true; _alignment = Alignment.bottomLeft; final bottomRight = selectionRects.first.bottomRight; @@ -91,20 +91,24 @@ class SelectionMenu implements SelectionMenuService { top: showBelow ? _offset.dy : null, bottom: showBelow ? null : _offset.dy, left: offset.dx, - child: SelectionMenuWidget( - items: [ - ..._defaultSelectionMenuItems, - ...editorState.selectionMenuItems, - ], - maxItemInRow: 5, - editorState: editorState, - menuService: this, - onExit: () { - dismiss(); - }, - onSelectionUpdate: () { - _selectionUpdateByInner = true; - }, + right: 0, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SelectionMenuWidget( + items: [ + ..._defaultSelectionMenuItems, + ...editorState.selectionMenuItems, + ], + maxItemInRow: 5, + editorState: editorState, + menuService: this, + onExit: () { + dismiss(); + }, + onSelectionUpdate: () { + _selectionUpdateByInner = true; + }, + ), ), ); }); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 3a88cfcd4a..e8a4eca357 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -324,7 +323,7 @@ class _SelectionMenuWidgetState extends State { _deleteLastCharacters(); return KeyEventResult.handled; } else if (event.character != null && - !arrowKeys.contains(event.logicalKey)) { + !arrowKeys.contains(event.logicalKey) && event.logicalKey != LogicalKeyboardKey.tab) { keyword += event.character!; _insertText(event.character!); return KeyEventResult.handled; @@ -339,7 +338,14 @@ class _SelectionMenuWidgetState extends State { newSelectedIndex -= 1; } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { newSelectedIndex += 1; + } else if (event.logicalKey == LogicalKeyboardKey.tab) { + newSelectedIndex += widget.maxItemInRow; + var currRow = (newSelectedIndex) % widget.maxItemInRow; + if (newSelectedIndex >= _showingItems.length) { + newSelectedIndex = (currRow + 1) % widget.maxItemInRow; + } } + if (newSelectedIndex != _selectedIndex) { setState(() { _selectedIndex = newSelectedIndex.clamp(0, _showingItems.length - 1); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart index 44fabea573..19f70b2a92 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/style/plugin_styles.dart @@ -2,14 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:flutter/material.dart'; -Iterable> get lightPlguinStyleExtension => [ +Iterable> get lightPluginStyleExtension => [ HeadingPluginStyle.light, CheckboxPluginStyle.light, NumberListPluginStyle.light, QuotedTextPluginStyle.light, ]; -Iterable> get darkPlguinStyleExtension => [ +Iterable> get darkPluginStyleExtension => [ HeadingPluginStyle.dark, CheckboxPluginStyle.dark, NumberListPluginStyle.dark, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart index 93be8b0240..fc9df7346f 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -16,14 +16,14 @@ class ToolbarWidget extends StatefulWidget { required this.layerLink, required this.offset, required this.items, - this.aligment = Alignment.topLeft, + this.alignment = Alignment.topLeft, }) : super(key: key); final EditorState editorState; final LayerLink layerLink; final Offset offset; final List items; - final Alignment aligment; + final Alignment alignment; @override State createState() => _ToolbarWidgetState(); @@ -41,7 +41,7 @@ class _ToolbarWidgetState extends State with ToolbarMixin { link: widget.layerLink, showWhenUnlinked: true, offset: widget.offset, - followerAnchor: widget.aligment, + followerAnchor: widget.alignment, child: _buildToolbar(context), ), ); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart index 9fcfcfd108..86adfdecf1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -39,7 +39,7 @@ class AppFlowyEditor extends StatefulWidget { this.themeData = themeData ?? ThemeData.light().copyWith(extensions: [ ...lightEditorStyleExtension, - ...lightPlguinStyleExtension, + ...lightPluginStyleExtension, ]); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 3110a0e559..2a72400b1d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -232,7 +232,7 @@ void _pasteSingleLine( /// parse url from the line text /// reference: https://stackoverflow.com/questions/59444837/flutter-dart-regex-to-extract-urls-from-a-string Delta _lineContentToDelta(String lineContent) { - final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'); + final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\#\w/\-?=%.]+'); final Iterable matches = exp.allMatches(lineContent); final delta = Delta(); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart index b38d838fed..1f798f4114 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart @@ -310,7 +310,7 @@ ShortcutEventHandler underscoreToItalicHandler = (editorState, event) { return KeyEventResult.handled; }; -ShortcutEventHandler doubleAsteriskToBoldHanlder = (editorState, event) { +ShortcutEventHandler doubleAsteriskToBoldHandler = (editorState, event) { final selectionService = editorState.service.selectionService; final selection = selectionService.currentSelection.value; final textNodes = selectionService.currentSelectedNodes.whereType(); @@ -366,8 +366,8 @@ ShortcutEventHandler doubleAsteriskToBoldHanlder = (editorState, event) { return KeyEventResult.handled; }; -//Implement in the same way as doubleAsteriskToBoldHanlder -ShortcutEventHandler doubleUnderscoreToBoldHanlder = (editorState, event) { +//Implement in the same way as doubleAsteriskToBoldHandler +ShortcutEventHandler doubleUnderscoreToBoldHandler = (editorState, event) { final selectionService = editorState.service.selectionService; final selection = selectionService.currentSelection.value; final textNodes = selectionService.currentSelectedNodes.whereType(); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart index 800877b435..bc130fd6bf 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart @@ -8,7 +8,9 @@ ShortcutEventHandler selectAllHandler = (editorState, event) { if (editorState.document.root.children.isEmpty) { return KeyEventResult.handled; } - final firstNode = editorState.document.root.children.first; + final firstNode = editorState.document.root.children.firstWhere( + (element) => element is TextNode, + ); final lastNode = editorState.document.root.children.last; var offset = 0; if (lastNode is TextNode) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart index b903fed4f8..bcd0f769fb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -121,15 +121,13 @@ class _AppFlowyKeyboardState extends State return KeyEventResult.ignored; } - Log.keyboard.debug('on keyboard event $event'); - if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } // TODO: use cache to optimize the searching time. for (final shortcutEvent in widget.shortcutEvents) { - if (shortcutEvent.keybindings.containsKeyEvent(event)) { + if (shortcutEvent.canRespondToRawKeyEvent(event)) { final result = shortcutEvent.handler(widget.editorState, event); if (result == KeyEventResult.handled) { return KeyEventResult.handled; @@ -157,3 +155,10 @@ class _AppFlowyKeyboardState extends State return onKey(event); } } + +extension on ShortcutEvent { + bool canRespondToRawKeyEvent(RawKeyEvent event) { + return ((character?.isNotEmpty ?? false) && character == event.character) || + keybindings.containsKeyEvent(event); + } +} diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart index a2e716c9e2..7578407a09 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -82,6 +82,13 @@ abstract class AppFlowySelectionService { /// The current selection areas's rect in editor. List get selectionRects; + + void register(SelectionInterceptor interceptor); + void unRegister(SelectionInterceptor interceptor); +} + +class SelectionInterceptor { + bool Function(TapDownDetails details)? canTap; } class AppFlowySelection extends StatefulWidget { @@ -212,6 +219,7 @@ class _AppFlowySelectionState extends State selectionRects.clear(); clearSelection(); + _clearToolbar(); if (selection != null) { if (selection.isCollapsed) { @@ -286,6 +294,10 @@ class _AppFlowySelectionState extends State } void _onTapDown(TapDownDetails details) { + final canTap = + _interceptors.every((element) => element.canTap?.call(details) ?? true); + if (!canTap) return; + // clear old state. _panStartOffset = null; @@ -701,4 +713,15 @@ class _AppFlowySelectionState extends State // } // } } + + final List _interceptors = []; + @override + void register(SelectionInterceptor interceptor) { + _interceptors.add(interceptor); + } + + @override + void unRegister(SelectionInterceptor interceptor) { + _interceptors.removeWhere((element) => element == interceptor); + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index d6338b6fe3..3d8fa6839d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -247,7 +247,7 @@ List builtInShortcutEvents = [ ), ShortcutEvent( key: 'selection menu', - command: 'slash,shift+slash', + character: '/', handler: slashShortcutHandler, ), ShortcutEvent( @@ -289,7 +289,7 @@ List builtInShortcutEvents = [ ), ShortcutEvent( key: 'Double tilde to strikethrough', - command: 'tilde,shift+tilde', + character: '~', handler: doubleTildeToStrikethrough, ), ShortcutEvent( @@ -304,18 +304,18 @@ List builtInShortcutEvents = [ ), ShortcutEvent( key: 'Underscore to italic', - command: 'shift+underscore', + character: '_', handler: underscoreToItalicHandler, ), ShortcutEvent( key: 'Double asterisk to bold', - command: 'shift+digit 8', - handler: doubleAsteriskToBoldHanlder, + character: '*', + handler: doubleAsteriskToBoldHandler, ), ShortcutEvent( key: 'Double underscore to bold', - command: 'shift+underscore', - handler: doubleUnderscoreToBoldHanlder, + character: '_', + handler: doubleUnderscoreToBoldHandler, ), // https://github.com/flutter/flutter/issues/104944 // Workaround: Using space editing on the web platform often results in errors, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart index fb1a245b00..bba5aff1c3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart @@ -8,12 +8,29 @@ import 'package:flutter/foundation.dart'; class ShortcutEvent { ShortcutEvent({ required this.key, - required this.command, + this.character, + this.command, required this.handler, String? windowsCommand, String? macOSCommand, String? linuxCommand, }) { + // character and command cannot be null at the same time + assert( + !(character == null && + command == null && + windowsCommand == null && + macOSCommand == null && + linuxCommand == null), + 'character and command cannot be null at the same time'); + assert( + !(character != null && + (command != null && + windowsCommand != null && + macOSCommand != null && + linuxCommand != null)), + 'character and command cannot be set at the same time'); + updateCommand( command: command, windowsCommand: windowsCommand, @@ -43,7 +60,9 @@ class ShortcutEvent { /// /// Like, 'ctrl+c,cmd+c' /// - String command; + String? command; + + String? character; final ShortcutEventHandler handler; @@ -80,9 +99,9 @@ class ShortcutEvent { matched = true; } - if (matched) { + if (matched && this.command != null) { _keybindings = this - .command + .command! .split(',') .map((e) => Keybinding.parse(e)) .toList(growable: false); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 9fd8ca3648..2d63ac8158 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -66,7 +66,7 @@ class _FlowyToolbarState extends State layerLink: layerLink, offset: offset, items: items, - aligment: alignment, + alignment: alignment, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart index 1c7325987b..45e5ed24f0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart @@ -4,7 +4,7 @@ import '../infra/test_editor.dart'; void main() { group('command_extension.dart', () { - testWidgets('insert a new checkbox after an exsiting checkbox', + testWidgets('insert a new checkbox after an existing checkbox', (tester) async { final editor = tester.editor ..insertTextNode( @@ -26,11 +26,11 @@ void main() { .editorState.service.selectionService.currentSelectedNodes .whereType() .toList(growable: false); - final text = editor.editorState.getTextInSelection( + final texts = editor.editorState.getTextInSelection( textNodes.normalized, selection.normalized, ); - expect(text, 'me\nto\nAppfl'); + expect(texts, ['me', 'to', 'Appfl']); }); }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart index 27a3701d84..bfb9fa2b3a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart @@ -91,6 +91,43 @@ void main() async { expect(textNodes[3].toPlainText(), 'ABC456789'); }); + testWidgets('test replaceTexts, textNodes.length >> texts.length', + (tester) async { + TestWidgetsFlutterBinding.ensureInitialized(); + + final editor = tester.editor + ..insertTextNode('0123456789') + ..insertTextNode('0123456789') + ..insertTextNode('0123456789') + ..insertTextNode('0123456789') + ..insertTextNode('0123456789'); + await editor.startTesting(); + await tester.pumpAndSettle(); + + expect(editor.documentLength, 5); + + final selection = Selection( + start: Position(path: [0], offset: 4), + end: Position(path: [4], offset: 4), + ); + final transaction = editor.editorState.transaction; + var textNodes = [0, 1, 2, 3, 4] + .map((e) => editor.nodeAtPath([e])!) + .whereType() + .toList(growable: false); + final texts = ['ABC']; + transaction.replaceTexts(textNodes, selection, texts); + editor.editorState.apply(transaction); + await tester.pumpAndSettle(); + + expect(editor.documentLength, 1); + textNodes = [0] + .map((e) => editor.nodeAtPath([e])!) + .whereType() + .toList(growable: false); + expect(textNodes[0].toPlainText(), '0123ABC456789'); + }); + testWidgets('test replaceTexts, textNodes.length < texts.length', (tester) async { TestWidgetsFlutterBinding.ensureInitialized(); @@ -128,5 +165,42 @@ void main() async { expect(textNodes[2].toPlainText(), 'ABC'); expect(textNodes[3].toPlainText(), 'ABC456789'); }); + + testWidgets('test replaceTexts, textNodes.length << texts.length', + (tester) async { + TestWidgetsFlutterBinding.ensureInitialized(); + + final editor = tester.editor..insertTextNode('Welcome to AppFlowy!'); + await editor.startTesting(); + await tester.pumpAndSettle(); + + expect(editor.documentLength, 1); + + // select 'to' + final selection = Selection( + start: Position(path: [0], offset: 8), + end: Position(path: [0], offset: 10), + ); + final transaction = editor.editorState.transaction; + var textNodes = [0] + .map((e) => editor.nodeAtPath([e])!) + .whereType() + .toList(growable: false); + final texts = ['ABC1', 'ABC2', 'ABC3', 'ABC4', 'ABC5']; + transaction.replaceTexts(textNodes, selection, texts); + editor.editorState.apply(transaction); + await tester.pumpAndSettle(); + + expect(editor.documentLength, 5); + textNodes = [0, 1, 2, 3, 4] + .map((e) => editor.nodeAtPath([e])!) + .whereType() + .toList(growable: false); + expect(textNodes[0].toPlainText(), 'Welcome ABC1'); + expect(textNodes[1].toPlainText(), 'ABC2'); + expect(textNodes[2].toPlainText(), 'ABC3'); + expect(textNodes[3].toPlainText(), 'ABC4'); + expect(textNodes[4].toPlainText(), 'ABC5 AppFlowy!'); + }); }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart index 3c8b3b0cc0..9aa3db6bb2 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/extensions/node_extension_test.dart @@ -45,7 +45,7 @@ void main() { expect(result, false); }); - testWidgets('insert a new checkbox after an exsiting checkbox', + testWidgets('insert a new checkbox after an existing checkbox', (tester) async { const text = 'Welcome to Appflowy 😁'; final editor = tester.editor diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart index 55672fba47..910e101b63 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_editor.dart @@ -120,8 +120,9 @@ class EditorWidgetTester { await tester.pumpAndSettle(); } - Future pressLogicKey( - LogicalKeyboardKey key, { + Future pressLogicKey({ + String? character, + LogicalKeyboardKey? key, bool isControlPressed = false, bool isShiftPressed = false, bool isAltPressed = false, @@ -130,11 +131,13 @@ class EditorWidgetTester { if (!isControlPressed && !isShiftPressed && !isAltPressed && - !isMetaPressed) { + !isMetaPressed && + key != null) { await tester.sendKeyDownEvent(key); } else { final testRawKeyEventData = TestRawKeyEventData( - logicalKey: key, + logicalKey: key ?? LogicalKeyboardKey.nonConvert, + character: character, isControlPressed: isControlPressed, isShiftPressed: isShiftPressed, isAltPressed: isAltPressed, @@ -160,7 +163,7 @@ class EditorWidgetTester { ), ) ..disableSealTimer = true - ..disbaleRules = true; + ..disableRules = true; } bool runAction(int actionIndex, Node node) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 0a3d6cd74f..2b62a7022a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; class TestRawKeyEvent extends RawKeyDownEvent { const TestRawKeyEvent({ required super.data, + required super.character, this.isControlPressed = false, this.isShiftPressed = false, this.isAltPressed = false, @@ -24,7 +25,9 @@ class TestRawKeyEvent extends RawKeyDownEvent { class TestRawKeyEventData extends RawKeyEventData { const TestRawKeyEventData({ - required this.logicalKey, + this.logicalKey = + LogicalKeyboardKey.nonConvert, // use nonConvert as placeholder. + this.character, this.isControlPressed = false, this.isShiftPressed = false, this.isAltPressed = false, @@ -46,6 +49,8 @@ class TestRawKeyEventData extends RawKeyEventData { @override final LogicalKeyboardKey logicalKey; + final String? character; + @override PhysicalKeyboardKey get physicalKey => logicalKey.toPhysicalKey; @@ -55,8 +60,10 @@ class TestRawKeyEventData extends RawKeyEventData { } @override - bool isModifierPressed(ModifierKey key, - {KeyboardSide side = KeyboardSide.any}) { + bool isModifierPressed( + ModifierKey key, { + KeyboardSide side = KeyboardSide.any, + }) { throw UnimplementedError(); } @@ -66,6 +73,7 @@ class TestRawKeyEventData extends RawKeyEventData { RawKeyEvent get toKeyEvent { return TestRawKeyEvent( data: this, + character: character, isAltPressed: isAltPressed, isControlPressed: isControlPressed, isMetaPressed: isMetaPressed, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart index eceb429894..b3e22f89e6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart @@ -71,7 +71,7 @@ void main() async { // https://github.com/AppFlowy-IO/AppFlowy/issues/1763 // // [Bug] Mouse unable to click a certain area #1763 - testWidgets('insert a new checkbox after an exsiting checkbox', + testWidgets('insert a new checkbox after an existing checkbox', (tester) async { // Before // @@ -100,16 +100,16 @@ void main() async { Selection.single(path: [0], startOffset: text.length), ); - await editor.pressLogicKey(LogicalKeyboardKey.enter); - await editor.pressLogicKey(LogicalKeyboardKey.enter); - await editor.pressLogicKey(LogicalKeyboardKey.enter); + await editor.pressLogicKey(key: LogicalKeyboardKey.enter); + await editor.pressLogicKey(key: LogicalKeyboardKey.enter); + await editor.pressLogicKey(key: LogicalKeyboardKey.enter); expect( editor.documentSelection, Selection.single(path: [2], startOffset: 0), ); - await editor.pressLogicKey(LogicalKeyboardKey.slash); + await editor.pressLogicKey(key: LogicalKeyboardKey.slash); await tester.pumpAndSettle(const Duration(milliseconds: 1000)); expect( diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 6b392cdebb..188663665e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; @@ -22,10 +21,10 @@ void main() async { (tester) async { final editor = await _prepare(tester); for (var j = 0; j < i; j++) { - await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowDown); } - await editor.pressLogicKey(LogicalKeyboardKey.enter); + await editor.pressLogicKey(key: LogicalKeyboardKey.enter); expect( find.byType(SelectionMenuWidget, skipOffstage: false), findsNothing, @@ -52,33 +51,33 @@ void main() async { testWidgets('Search item in selection menu util no results', (tester) async { final editor = await _prepare(tester); - await editor.pressLogicKey(LogicalKeyboardKey.keyT); - await editor.pressLogicKey(LogicalKeyboardKey.keyE); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyT); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNWidgets(3), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNWidgets(5), ); - await editor.pressLogicKey(LogicalKeyboardKey.keyE); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNWidgets(3), ); - await editor.pressLogicKey(LogicalKeyboardKey.keyX); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyX); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNWidgets(1), ); - await editor.pressLogicKey(LogicalKeyboardKey.keyT); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyT); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNWidgets(1), ); - await editor.pressLogicKey(LogicalKeyboardKey.keyT); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyT); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNothing, @@ -88,13 +87,13 @@ void main() async { testWidgets('Search item in selection menu and presses esc', (tester) async { final editor = await _prepare(tester); - await editor.pressLogicKey(LogicalKeyboardKey.keyT); - await editor.pressLogicKey(LogicalKeyboardKey.keyE); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyT); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNWidgets(3), ); - await editor.pressLogicKey(LogicalKeyboardKey.escape); + await editor.pressLogicKey(key: LogicalKeyboardKey.escape); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNothing, @@ -104,20 +103,92 @@ void main() async { testWidgets('Search item in selection menu and presses backspace', (tester) async { final editor = await _prepare(tester); - await editor.pressLogicKey(LogicalKeyboardKey.keyT); - await editor.pressLogicKey(LogicalKeyboardKey.keyE); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyT); + await editor.pressLogicKey(key: LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNWidgets(3), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), findsNothing, ); }); + + group('tab and arrow keys move selection in desired direction', () { + testWidgets('left and right keys move selection in desired direction', + (tester) async { + final editor = await _prepare(tester); + + var initialSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0); + + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowRight); + + var newSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(newSelection.item), 5); + + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowLeft); + + var finalSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0); + }); + + testWidgets('up and down keys move selection in desired direction', + (tester) async { + final editor = await _prepare(tester); + + var initialSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0); + + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowDown); + + var newSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(newSelection.item), 1); + + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowUp); + + var finalSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(finalSelection.item), 0); + }); + + testWidgets('arrow keys and tab move same selection', (tester) async { + final editor = await _prepare(tester); + + var initialSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0); + + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowDown); + + var newSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(newSelection.item), 1); + + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); + + var finalSelection = getSelectedMenuItem(tester); + expect(defaultSelectionMenuItems.indexOf(finalSelection.item), 6); + }); + + testWidgets( + 'tab moves selection to next row Item on reaching end of current row', + (tester) async { + final editor = await _prepare(tester); + + final initialSelection = getSelectedMenuItem(tester); + + expect(defaultSelectionMenuItems.indexOf(initialSelection.item), 0); + + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); + + final finalSelection = getSelectedMenuItem(tester); + + expect(defaultSelectionMenuItems.indexOf(finalSelection.item), 1); + }); + }); }); } @@ -130,7 +201,7 @@ Future _prepare(WidgetTester tester) async { } await editor.startTesting(); await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); - await editor.pressLogicKey(LogicalKeyboardKey.slash); + await editor.pressLogicKey(key: LogicalKeyboardKey.slash); await tester.pumpAndSettle(const Duration(milliseconds: 1000)); @@ -178,3 +249,11 @@ Future _testDefaultSelectionMenuItems( expect(node?.attributes.check, false); } } + +SelectionMenuItemWidget getSelectedMenuItem(WidgetTester tester) { + return tester + .state(find.byWidgetPredicate( + (widget) => widget is SelectionMenuItemWidget && widget.isSelected, + )) + .widget as SelectionMenuItemWidget; +} diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart index 25e633e490..75b6696d29 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart @@ -25,7 +25,7 @@ void main() async { final textNode = editor.nodeAtPath([0]) as TextNode; for (var i = 0; i < text.length; i++) { - await editor.pressLogicKey(LogicalKeyboardKey.arrowRight); + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowRight); if (i == text.length - 1) { // Wrap to next node if the cursor is at the end of the current node. @@ -73,7 +73,7 @@ void main() async { await editor.updateSelection(selection); for (var i = offset - 1; i >= 0; i--) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, ); expect( @@ -85,7 +85,7 @@ void main() async { } for (var i = text.length; i >= 0; i--) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, ); expect( @@ -97,7 +97,7 @@ void main() async { } for (var i = 1; i <= text.length; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, ); expect( @@ -109,7 +109,7 @@ void main() async { } for (var i = 0; i < text.length; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, ); expect( @@ -139,7 +139,7 @@ void main() async { await editor.updateSelection(selection); for (var i = end + 1; i <= text.length; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, ); expect( @@ -151,7 +151,7 @@ void main() async { } for (var i = text.length - 1; i >= 0; i--) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, ); expect( @@ -181,7 +181,7 @@ void main() async { await editor.updateSelection(selection); for (var i = end - 1; i >= 0; i--) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, ); expect( @@ -193,7 +193,7 @@ void main() async { } for (var i = 1; i <= text.length; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, ); expect( @@ -238,7 +238,7 @@ void main() async { await editor.updateSelection(selection); for (int i = 0; i < 3; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowUp, + key: LogicalKeyboardKey.arrowUp, isShiftPressed: true, ); } @@ -250,7 +250,7 @@ void main() async { ); for (int i = 0; i < 7; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowDown, + key: LogicalKeyboardKey.arrowDown, isShiftPressed: true, ); } @@ -262,7 +262,7 @@ void main() async { ); for (int i = 0; i < 3; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowUp, + key: LogicalKeyboardKey.arrowUp, isShiftPressed: true, ); } @@ -284,18 +284,18 @@ void main() async { final selection = Selection.single(path: [0], startOffset: 8); await editor.updateSelection(selection); await editor.pressLogicKey( - LogicalKeyboardKey.arrowDown, + key: LogicalKeyboardKey.arrowDown, isShiftPressed: true, ); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, isMetaPressed: true, ); @@ -318,18 +318,18 @@ void main() async { final selection = Selection.single(path: [1], startOffset: 8); await editor.updateSelection(selection); await editor.pressLogicKey( - LogicalKeyboardKey.arrowUp, + key: LogicalKeyboardKey.arrowUp, isShiftPressed: true, ); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, isMetaPressed: true, ); @@ -352,7 +352,7 @@ void main() async { final selection = Selection.single(path: [1], startOffset: 10); await editor.updateSelection(selection); await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, isAltPressed: true, ); @@ -364,7 +364,7 @@ void main() async { ), ); await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, isAltPressed: true, ); @@ -376,7 +376,7 @@ void main() async { ), ); await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, isAltPressed: true, ); @@ -388,7 +388,7 @@ void main() async { ), ); await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isShiftPressed: true, isAltPressed: true, ); @@ -412,7 +412,7 @@ void main() async { final selection = Selection.single(path: [0], startOffset: 10); await editor.updateSelection(selection); await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, isAltPressed: true, ); @@ -424,7 +424,7 @@ void main() async { ), ); await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, isAltPressed: true, ); @@ -436,12 +436,12 @@ void main() async { ), ); await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, isAltPressed: true, ); await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, isAltPressed: true, ); @@ -453,7 +453,7 @@ void main() async { ), ); await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isShiftPressed: true, isAltPressed: true, ); @@ -478,12 +478,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isMetaPressed: true, ); } @@ -499,12 +499,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isMetaPressed: true, ); } @@ -521,12 +521,12 @@ void main() async { for (var i = 0; i < words.length; i++) { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isMetaPressed: true, ); } @@ -550,12 +550,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isMetaPressed: true, ); } @@ -575,12 +575,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, isMetaPressed: true, ); } @@ -610,11 +610,11 @@ Future _testPressArrowKeyInNotCollapsedSelection( end: isBackward ? end : start, ); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.arrowLeft); + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowLeft); expect(editor.documentSelection?.start, start); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.arrowRight); + await editor.pressLogicKey(key: LogicalKeyboardKey.arrowRight); expect(editor.documentSelection?.end, end); } @@ -652,12 +652,12 @@ Future _testPressArrowKeyWithMetaInSelection( await editor.updateSelection(selection); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isMetaPressed: true, ); } @@ -669,12 +669,12 @@ Future _testPressArrowKeyWithMetaInSelection( if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isMetaPressed: true, ); } @@ -686,12 +686,12 @@ Future _testPressArrowKeyWithMetaInSelection( if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowUp, + key: LogicalKeyboardKey.arrowUp, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowUp, + key: LogicalKeyboardKey.arrowUp, isMetaPressed: true, ); } @@ -703,12 +703,12 @@ Future _testPressArrowKeyWithMetaInSelection( if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowDown, + key: LogicalKeyboardKey.arrowDown, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowDown, + key: LogicalKeyboardKey.arrowDown, isMetaPressed: true, ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index e786377de0..b299c18aa4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -30,7 +30,7 @@ void main() async { // Pressing the backspace key continuously. for (int i = 1; i <= 1; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, ); expect(editor.documentLength, 1); expect(editor.documentSelection, @@ -112,7 +112,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0], startOffset: text.length), ); - await editor.pressLogicKey(LogicalKeyboardKey.delete); + await editor.pressLogicKey(key: LogicalKeyboardKey.delete); expect(editor.documentLength, 1); expect(editor.documentSelection, @@ -207,7 +207,7 @@ void main() async { ), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.documentLength, 3); expect(find.byType(ImageNodeWidget), findsNothing); expect( @@ -249,20 +249,20 @@ void main() async { final textNode = editor.nodeAtPath([0]) as TextNode; await editor.insertText(textNode, '#', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect( (editor.nodeAtPath([0]) as TextNode).attributes.heading, BuiltInAttributeKey.h1, ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect( textNode.attributes.heading, null, ); await editor.insertText(textNode, '#', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect( (editor.nodeAtPath([0]) as TextNode).attributes.heading, BuiltInAttributeKey.h1, @@ -296,17 +296,17 @@ void main() async { await editor.updateSelection( Selection.single(path: [0, 0, 0], startOffset: 0), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.nodeAtPath([0, 0, 0])?.subtype, null); await editor.updateSelection( Selection.single(path: [0, 0, 0], startOffset: 0), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.nodeAtPath([0, 1]) != null, true); await editor.updateSelection( Selection.single(path: [0, 1], startOffset: 0), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.nodeAtPath([1]) != null, true); await editor.updateSelection( Selection.single(path: [1], startOffset: 0), @@ -314,7 +314,7 @@ void main() async { // * Welcome to Appflowy 😁 // * Welcome to Appflowy 😁Welcome to Appflowy 😁 - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect( editor.documentSelection, Selection.single(path: [0, 0], startOffset: text.length), @@ -356,7 +356,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0, 1], startOffset: 0), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect( editor.nodeAtPath([0, 1])!.subtype != BuiltInAttributeKey.bulletedList, true, @@ -382,7 +382,7 @@ void main() async { // * Welcome to Appflowy 😁Welcome to Appflowy 😁 // * Welcome to Appflowy 😁 // * Welcome to Appflowy 😁 - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect( editor.nodeAtPath([0, 0])!.subtype == BuiltInAttributeKey.bulletedList, true, @@ -424,7 +424,7 @@ Future _deleteFirstImage(WidgetTester tester, bool isBackward) async { ), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.documentLength, 2); expect(find.byType(ImageNodeWidget), findsNothing); expect(editor.documentSelection, Selection.collapsed(start)); @@ -453,7 +453,7 @@ Future _deleteLastImage(WidgetTester tester, bool isBackward) async { ), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.documentLength, 2); expect(find.byType(ImageNodeWidget), findsNothing); expect(editor.documentSelection, Selection.collapsed(start)); @@ -483,12 +483,12 @@ Future _deleteStyledTextByBackspace( Selection.single(path: [2], startOffset: 0), ); await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, ); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, ); expect(editor.documentLength, 2); expect(editor.documentSelection, @@ -500,7 +500,7 @@ Future _deleteStyledTextByBackspace( Selection.single(path: [1], startOffset: 0), ); await editor.pressLogicKey( - LogicalKeyboardKey.backspace, + key: LogicalKeyboardKey.backspace, ); expect(editor.documentLength, 2); expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); @@ -531,7 +531,7 @@ Future _deleteStyledTextByDelete( ); for (var i = 1; i < text.length; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.delete, + key: LogicalKeyboardKey.delete, ); expect( editor.documentSelection, Selection.single(path: [1], startOffset: 0)); @@ -541,7 +541,7 @@ Future _deleteStyledTextByDelete( } await editor.pressLogicKey( - LogicalKeyboardKey.delete, + key: LogicalKeyboardKey.delete, ); expect(editor.documentLength, 2); expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); @@ -562,7 +562,7 @@ Future _deleteTextByBackspace( await editor.updateSelection( Selection.single(path: [1], startOffset: 10), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); @@ -573,7 +573,7 @@ Future _deleteTextByBackspace( await editor.updateSelection( Selection.single(path: [2], startOffset: 8, endOffset: 11), ); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); expect((editor.nodeAtPath([2]) as TextNode).toPlainText(), @@ -587,7 +587,7 @@ Future _deleteTextByBackspace( await editor.updateSelection(Selection( start: isBackwardSelection ? start : end, end: isBackwardSelection ? end : start)); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.documentLength, 1); expect( editor.documentSelection, Selection.single(path: [0], startOffset: 11)); @@ -608,7 +608,7 @@ Future _deleteTextByDelete( await editor.updateSelection( Selection.single(path: [1], startOffset: 9), ); - await editor.pressLogicKey(LogicalKeyboardKey.delete); + await editor.pressLogicKey(key: LogicalKeyboardKey.delete); expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); @@ -619,7 +619,7 @@ Future _deleteTextByDelete( await editor.updateSelection( Selection.single(path: [2], startOffset: 8, endOffset: 11), ); - await editor.pressLogicKey(LogicalKeyboardKey.delete); + await editor.pressLogicKey(key: LogicalKeyboardKey.delete); expect(editor.documentLength, 3); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); expect((editor.nodeAtPath([2]) as TextNode).toPlainText(), @@ -633,7 +633,7 @@ Future _deleteTextByDelete( await editor.updateSelection(Selection( start: isBackwardSelection ? start : end, end: isBackwardSelection ? end : start)); - await editor.pressLogicKey(LogicalKeyboardKey.delete); + await editor.pressLogicKey(key: LogicalKeyboardKey.delete); expect(editor.documentLength, 1); expect( editor.documentSelection, Selection.single(path: [0], startOffset: 11)); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/checkbox_event_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/checkbox_event_handler_test.dart index e1fd1f6ccc..03d5c78d04 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/checkbox_event_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/checkbox_event_handler_test.dart @@ -47,12 +47,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isMetaPressed: true, ); } @@ -65,12 +65,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isMetaPressed: true, ); } @@ -145,12 +145,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isMetaPressed: true, ); } @@ -223,12 +223,12 @@ void main() async { if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, isMetaPressed: true, ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index 74797b8402..9bca3748d6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -26,7 +26,7 @@ void main() async { // Pressing the enter key continuously. for (int i = 1; i <= 10; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, ); expect(editor.documentLength, i + 1); expect(editor.documentSelection, @@ -64,7 +64,7 @@ void main() async { Selection.single(path: [lines - 1], startOffset: 0), ); await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, ); lines += 1; expect(editor.documentLength, lines); @@ -132,7 +132,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0], startOffset: 0), ); - await editor.pressLogicKey(LogicalKeyboardKey.enter); + await editor.pressLogicKey(key: LogicalKeyboardKey.enter); expect(editor.documentLength, 2); expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), text); }); @@ -159,7 +159,7 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { Selection.single(path: [1], startOffset: 0), ); await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, ); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); @@ -167,7 +167,7 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { Selection.single(path: [3], startOffset: text.length), ); await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, ); expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); @@ -176,7 +176,7 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { expect(editor.nodeAtPath([4])?.subtype, null); await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, ); expect( editor.documentSelection, Selection.single(path: [5], startOffset: 0)); @@ -185,7 +185,7 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { expect(editor.nodeAtPath([4])?.subtype, style); await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, ); expect( editor.documentSelection, Selection.single(path: [4], startOffset: 0)); @@ -223,7 +223,7 @@ Future _testMultipleSelection( end: isBackwardSelection ? end : start, )); await editor.pressLogicKey( - LogicalKeyboardKey.enter, + key: LogicalKeyboardKey.enter, ); expect(editor.documentLength, 2); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart index 6ee51e4da6..0d264995cc 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/exit_editing_mode_handler_test.dart @@ -27,7 +27,7 @@ void main() async { Selection.single(path: [1], startOffset: 0, endOffset: text.length), ); - // mutliple selection + // multiple selection await _testSelection( editor, Selection( @@ -45,6 +45,6 @@ Future _testSelection( EditorWidgetTester editor, Selection selection) async { await editor.updateSelection(selection); expect(editor.documentSelection, selection); - await editor.pressLogicKey(LogicalKeyboardKey.escape); + await editor.pressLogicKey(key: LogicalKeyboardKey.escape); expect(editor.documentSelection, null); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart index 222a7efe1b..842fcc6768 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart @@ -95,13 +95,13 @@ Future _testUpdateTextStyleByCommandX( await editor.updateSelection(selection); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isControlPressed: true, ); } else { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isMetaPressed: true, ); @@ -122,13 +122,13 @@ Future _testUpdateTextStyleByCommandX( await editor.updateSelection(selection); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isControlPressed: true, ); } else { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isMetaPressed: true, ); @@ -147,13 +147,13 @@ Future _testUpdateTextStyleByCommandX( await editor.updateSelection(selection); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isControlPressed: true, ); } else { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isMetaPressed: true, ); @@ -169,13 +169,13 @@ Future _testUpdateTextStyleByCommandX( await editor.updateSelection(selection); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isControlPressed: true, ); } else { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isMetaPressed: true, ); @@ -204,13 +204,13 @@ Future _testUpdateTextStyleByCommandX( if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isControlPressed: true, ); } else { await editor.pressLogicKey( - key, + key: key, isShiftPressed: isShiftPressed, isMetaPressed: true, ); @@ -250,9 +250,11 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { // trigger the link menu if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyK, isControlPressed: true); } else { - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyK, isMetaPressed: true); } expect(find.byType(LinkMenu), findsOneWidget); @@ -273,9 +275,11 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { await editor.updateSelection(selection); if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyK, isControlPressed: true); } else { - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyK, isMetaPressed: true); } expect(find.byType(LinkMenu), findsOneWidget); expect( @@ -290,9 +294,11 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { // Remove link if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyK, isControlPressed: true); } else { - await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyK, isMetaPressed: true); } final removeLink = find.text('Remove link'); expect(removeLink, findsOneWidget); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart index 219fd07568..c7bb897034 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart @@ -17,7 +17,7 @@ void main() async { }) async { for (var i = 0; i < repeat; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.backquote, + key: LogicalKeyboardKey.backquote, ); } } @@ -99,7 +99,7 @@ void main() async { }) async { for (var i = 0; i < repeat; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.backquote, + key: LogicalKeyboardKey.backquote, ); } } @@ -157,10 +157,7 @@ void main() async { int repeat = 1, }) async { for (var i = 0; i < repeat; i++) { - await editor.pressLogicKey( - LogicalKeyboardKey.tilde, - isShiftPressed: true, - ); + await editor.pressLogicKey(character: '~'); } } @@ -264,10 +261,7 @@ void main() async { int repeat = 1, }) async { for (var i = 0; i < repeat; i++) { - await editor.pressLogicKey( - LogicalKeyboardKey.digit8, - isShiftPressed: true, - ); + await editor.pressLogicKey(character: '*'); } } @@ -358,10 +352,7 @@ void main() async { int repeat = 1, }) async { for (var i = 0; i < repeat; i++) { - await editor.pressLogicKey( - LogicalKeyboardKey.underscore, - isShiftPressed: true, - ); + await editor.pressLogicKey(character: '_'); } } @@ -378,8 +369,13 @@ void main() async { await insertUnderscore(editor); - final allBold = textNode.allSatisfyBoldInSelection(Selection.single( - path: [0], startOffset: 0, endOffset: textNode.toPlainText().length)); + final allBold = textNode.allSatisfyBoldInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toPlainText().length, + ), + ); expect(allBold, true); expect(textNode.toPlainText(), 'AppFlowy'); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart index 465d198e8e..980d1ace20 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart @@ -38,7 +38,7 @@ void main() async { var currentOffsetY = 0.0; for (int i = 1; i <= page!; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.pageDown, + key: LogicalKeyboardKey.pageDown, ); if (i == page) { currentOffsetY = scrollService.maxScrollExtent; @@ -51,7 +51,7 @@ void main() async { for (int i = 1; i <= 5; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.pageDown, + key: LogicalKeyboardKey.pageDown, ); final dy = scrollService.dy; expect(dy == scrollService.maxScrollExtent, true); @@ -60,7 +60,7 @@ void main() async { // Pressing the pageUp key continuously. for (int i = page; i >= 1; i--) { await editor.pressLogicKey( - LogicalKeyboardKey.pageUp, + key: LogicalKeyboardKey.pageUp, ); if (i == 1) { currentOffsetY = scrollService.minScrollExtent; @@ -73,7 +73,7 @@ void main() async { for (int i = 1; i <= 5; i++) { await editor.pressLogicKey( - LogicalKeyboardKey.pageUp, + key: LogicalKeyboardKey.pageUp, ); final dy = scrollService.dy; expect(dy == scrollService.minScrollExtent, true); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart index 5e4260a4a1..d2a0bcbe2d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart @@ -40,17 +40,17 @@ Future _testBackspaceUndoRedo( end: isDownwardSelection ? end : start, ); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); + await editor.pressLogicKey(key: LogicalKeyboardKey.backspace); expect(editor.documentLength, 2); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.keyZ, + key: LogicalKeyboardKey.keyZ, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.keyZ, + key: LogicalKeyboardKey.keyZ, isMetaPressed: true, ); } @@ -61,13 +61,13 @@ Future _testBackspaceUndoRedo( if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.keyZ, + key: LogicalKeyboardKey.keyZ, isControlPressed: true, isShiftPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.keyZ, + key: LogicalKeyboardKey.keyZ, isMetaPressed: true, isShiftPressed: true, ); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart index 937988da58..25a38fd435 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart @@ -29,9 +29,11 @@ Future _testSelectAllHandler(WidgetTester tester, int lines) async { } await editor.startTesting(); if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey(LogicalKeyboardKey.keyA, isControlPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyA, isControlPressed: true); } else { - await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyA, isMetaPressed: true); } expect( diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart index a6e08d5fa8..d998022beb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; @@ -10,7 +9,8 @@ void main() async { }); group('slash_handler.dart', () { - testWidgets('Presses / to trigger selection menu', (tester) async { + testWidgets('Presses / to trigger selection menu in 0 index', + (tester) async { const text = 'Welcome to Appflowy 😁'; const lines = 3; final editor = tester.editor; @@ -19,7 +19,40 @@ void main() async { } await editor.startTesting(); await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); - await editor.pressLogicKey(LogicalKeyboardKey.slash); + await editor.pressLogicKey(key: LogicalKeyboardKey.slash); + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect( + find.byType(SelectionMenuWidget, skipOffstage: false), + findsOneWidget, + ); + + for (final item in defaultSelectionMenuItems) { + expect(find.text(item.name), findsOneWidget); + } + + await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); + + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect( + find.byType(SelectionMenuItemWidget, skipOffstage: false), + findsNothing, + ); + }); + + testWidgets('Presses / to trigger selection menu in not 0 index', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + const lines = 3; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [1], startOffset: 5)); + await editor.pressLogicKey(key: LogicalKeyboardKey.slash); await tester.pumpAndSettle(const Duration(milliseconds: 1000)); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart index a49d882fa3..4e326efa74 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart @@ -24,7 +24,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [i], startOffset: 1), ); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect( (editor.nodeAtPath([i]) as TextNode).toPlainText(), 'W elcome to Appflowy 😁', @@ -34,7 +34,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [i], startOffset: text.length + 1), ); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect( (editor.nodeAtPath([i]) as TextNode).toPlainText(), 'W elcome to Appflowy 😁 ', diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart index 4b88960c38..5ef746a2d0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart @@ -18,7 +18,7 @@ void main() async { var selection = Selection.single(path: [0], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -27,7 +27,7 @@ void main() async { selection = Selection.single(path: [1], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -61,7 +61,7 @@ void main() async { var selection = Selection.single(path: [0], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); // nothing happens expect( @@ -82,7 +82,7 @@ void main() async { selection = Selection.single(path: [1], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -96,7 +96,7 @@ void main() async { selection = Selection.single(path: [1], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -121,7 +121,7 @@ void main() async { document = editor.document; selection = Selection.single(path: [0, 0], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -131,7 +131,7 @@ void main() async { selection = Selection.single(path: [0, 1], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -182,7 +182,7 @@ void main() async { var selection = Selection.single(path: [0], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); // nothing happens expect( @@ -203,7 +203,7 @@ void main() async { selection = Selection.single(path: [1], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -216,7 +216,7 @@ void main() async { selection = Selection.single(path: [1], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -239,7 +239,7 @@ void main() async { document = editor.document; selection = Selection.single(path: [0, 0], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, @@ -249,7 +249,7 @@ void main() async { selection = Selection.single(path: [0, 1], startOffset: 0); await editor.updateSelection(selection); - await editor.pressLogicKey(LogicalKeyboardKey.tab); + await editor.pressLogicKey(key: LogicalKeyboardKey.tab); expect( editor.documentSelection, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart index 4349dc7a79..4418c4ed61 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart @@ -40,7 +40,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [i - 1], startOffset: i), ); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); final textNode = (editor.nodeAtPath([i - 1]) as TextNode); @@ -80,7 +80,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [i - 1], startOffset: i), ); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); final textNode = (editor.nodeAtPath([i - 1]) as TextNode); @@ -114,7 +114,7 @@ void main() async { final textNode = (editor.nodeAtPath([0]) as TextNode); await editor.insertText(textNode, '#' * i, 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.heading); // BuiltInAttributeKey.h2 ~ BuiltInAttributeKey.h6 @@ -134,7 +134,7 @@ void main() async { Selection.single(path: [0], startOffset: 0), ); await editor.insertText(textNode, symbol, 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.checkbox); expect(textNode.attributes.check, false); } @@ -152,7 +152,7 @@ void main() async { Selection.single(path: [0], startOffset: 0), ); await editor.insertText(textNode, symbol, 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.checkbox); expect(textNode.attributes.check, true); } @@ -169,7 +169,7 @@ void main() async { Selection.single(path: [0], startOffset: 0), ); await editor.insertText(textNode, symbol, 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.bulletedList); } }); @@ -185,34 +185,34 @@ void main() async { ); await editor.insertText(textNode, '>', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.quote); await editor.insertText(textNode, '*', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.bulletedList); await editor.insertText(textNode, '[]', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.checkbox); expect(textNode.attributes.check, false); await editor.insertText(textNode, '1.', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.numberList); await editor.insertText(textNode, '#', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.heading); await editor.insertText(textNode, '[x]', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.checkbox); expect(textNode.attributes.check, true); const insertedText = '[]AppFlowy'; await editor.insertText(textNode, insertedText, 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.checkbox); expect(textNode.attributes.check, true); expect(textNode.toPlainText(), insertedText); @@ -227,7 +227,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0], startOffset: text.length), ); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, null); expect(textNode.toPlainText(), text); }); @@ -243,7 +243,7 @@ void main() async { final textNode = editor.nodeAtPath([0]) as TextNode; await editor.insertText(textNode, '>', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); expect(textNode.subtype, BuiltInAttributeKey.quote); for (var i = 0; i < text.length; i++) { await editor.insertText(textNode, text[i], i); @@ -263,7 +263,7 @@ void main() async { for (var i = 0; i < text.length; i++) { await editor.insertText(textNode, text[i], i); } - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); final isQuote = textNode.subtype == BuiltInAttributeKey.quote; expect(isQuote, false); expect(textNode.toPlainText(), text); @@ -284,7 +284,7 @@ void main() async { Selection.single(path: [0], startOffset: 0), ); await editor.insertText(textNode, '>', 0); - await editor.pressLogicKey(LogicalKeyboardKey.space); + await editor.pressLogicKey(key: LogicalKeyboardKey.space); final isQuote = textNode.subtype == BuiltInAttributeKey.quote; expect(isQuote, true); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart index 034d91a93e..a8a827140d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart @@ -41,12 +41,12 @@ void main() async { ); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isMetaPressed: true, ); } @@ -69,12 +69,12 @@ void main() async { } if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isAltPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowLeft, + key: LogicalKeyboardKey.arrowLeft, isMetaPressed: true, ); } @@ -97,12 +97,12 @@ void main() async { ); if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isControlPressed: true, ); } else { await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isMetaPressed: true, ); } @@ -124,7 +124,7 @@ void main() async { } } await editor.pressLogicKey( - LogicalKeyboardKey.arrowRight, + key: LogicalKeyboardKey.arrowRight, isAltPressed: true, ); expect( @@ -145,7 +145,7 @@ void main() async { ); if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { await editor.pressLogicKey( - LogicalKeyboardKey.home, + key: LogicalKeyboardKey.home, ); } @@ -168,7 +168,7 @@ void main() async { } if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.home, + key: LogicalKeyboardKey.home, ); } expect( @@ -188,7 +188,7 @@ void main() async { ); if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { await editor.pressLogicKey( - LogicalKeyboardKey.end, + key: LogicalKeyboardKey.end, ); } @@ -211,7 +211,7 @@ void main() async { } if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { await editor.pressLogicKey( - LogicalKeyboardKey.end, + key: LogicalKeyboardKey.end, ); } expect( diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart index 86cd29705d..ce6430903d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -94,6 +94,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); testHighlight(false); await editor.updateSelection( @@ -103,6 +104,7 @@ void main() async { endOffset: text.length * 2, ), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); testHighlight(true); await editor.updateSelection( @@ -112,6 +114,7 @@ void main() async { endOffset: text.length * 2 - 2, ), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); testHighlight(true); }); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart index 34720d16f4..5f7e604a3e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart @@ -24,15 +24,15 @@ void _showEmojiSelectionMenu( SelectionMenuService menuService, BuildContext context, ) { - final aligment = menuService.alignment; + final alignment = menuService.alignment; final offset = menuService.offset; menuService.dismiss(); _emojiSelectionMenu?.remove(); _emojiSelectionMenu = OverlayEntry(builder: (context) { return Positioned( - top: aligment == Alignment.bottomLeft ? offset.dy : null, - bottom: aligment == Alignment.topLeft ? offset.dy : null, + top: alignment == Alignment.bottomLeft ? offset.dy : null, + bottom: alignment == Alignment.topLeft ? offset.dy : null, left: offset.dx, child: Material( child: EmojiSelectionMenu( diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index 3cb0972112..fdb3628011 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -72,6 +72,7 @@ class Popover extends StatefulWidget { final PopoverDirection direction; final void Function()? onClose; + final Future Function()? canClose; final bool asBarrier; @@ -92,6 +93,7 @@ class Popover extends StatefulWidget { this.mutex, this.windowPadding, this.onClose, + this.canClose, this.asBarrier = false, }) : super(key: key); @@ -122,7 +124,12 @@ class PopoverState extends State { children.add( PopoverMask( decoration: widget.maskDecoration, - onTap: () => _removeRootOverlay(), + onTap: () async { + if (!(await widget.canClose?.call() ?? true)) { + return; + } + _removeRootOverlay(); + }, onExit: () => _removeRootOverlay(), ), ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index bceadd6dd0..3673720220 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -10,11 +10,13 @@ class AppFlowyPopover extends StatelessWidget { final int triggerActions; final BoxConstraints constraints; final void Function()? onClose; + final Future Function()? canClose; final PopoverMutex? mutex; final Offset? offset; final bool asBarrier; final EdgeInsets margin; final EdgeInsets windowPadding; + final Decoration? decoration; const AppFlowyPopover({ Key? key, @@ -22,6 +24,7 @@ class AppFlowyPopover extends StatelessWidget { required this.popupBuilder, this.direction = PopoverDirection.rightWithTopAligned, this.onClose, + this.canClose, this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600), this.mutex, this.triggerActions = PopoverTriggerFlags.click, @@ -30,6 +33,7 @@ class AppFlowyPopover extends StatelessWidget { this.asBarrier = false, this.margin = const EdgeInsets.all(6), this.windowPadding = const EdgeInsets.all(8.0), + this.decoration, }) : super(key: key); @override @@ -37,6 +41,7 @@ class AppFlowyPopover extends StatelessWidget { return Popover( controller: controller, onClose: onClose, + canClose: canClose, direction: direction, mutex: mutex, asBarrier: asBarrier, @@ -49,6 +54,7 @@ class AppFlowyPopover extends StatelessWidget { return _PopoverContainer( constraints: constraints, margin: margin, + decoration: decoration, child: child, ); }, @@ -61,19 +67,23 @@ class _PopoverContainer extends StatelessWidget { final Widget child; final BoxConstraints constraints; final EdgeInsets margin; + final Decoration? decoration; + const _PopoverContainer({ required this.child, required this.margin, required this.constraints, + required this.decoration, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { - final decoration = FlowyDecoration.decoration( - Theme.of(context).colorScheme.surface, - Theme.of(context).colorScheme.shadow.withOpacity(0.15), - ); + final decoration = this.decoration ?? + FlowyDecoration.decoration( + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.shadow.withOpacity(0.15), + ); return Material( type: MaterialType.transparency, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart index e0158d5c0c..f8cc85938b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'dart:math'; -const _overlayContainerPadding = EdgeInsets.all(12); +const _overlayContainerPadding = EdgeInsets.symmetric(vertical: 12); const overlayContainerMaxWidth = 760.0; const overlayContainerMinWidth = 320.0; @@ -25,6 +25,7 @@ class FlowyDialog extends StatelessWidget { final windowSize = MediaQuery.of(context).size; final size = windowSize * 0.7; return SimpleDialog( + contentPadding: EdgeInsets.zero, title: title, shape: shape ?? RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), @@ -32,7 +33,6 @@ class FlowyDialog extends StatelessWidget { Material( type: MaterialType.transparency, child: Container( - padding: padding, height: size.height, width: max(min(size.width, overlayContainerMaxWidth), overlayContainerMinWidth), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index f880223728..f6e21acf11 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -121,7 +121,7 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: @@ -156,7 +156,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.4.3" + version: "8.4.4" calendar_view: dependency: "direct main" description: @@ -500,7 +500,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.9" flutter_svg: dependency: transitive description: @@ -524,7 +524,7 @@ packages: name: fluttertoast url: "https://pub.dartlang.org" source: hosted - version: "8.1.2" + version: "8.2.1" freezed: dependency: "direct dev" description: @@ -599,7 +599,7 @@ packages: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.1" + version: "0.15.2" http: dependency: "direct main" description: @@ -709,7 +709,7 @@ packages: name: logger url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.3.0" logging: dependency: transitive description: @@ -779,7 +779,7 @@ packages: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" package_config: dependency: transitive description: @@ -856,35 +856,35 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.14" path_provider_android: dependency: transitive description: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.22" + version: "2.0.24" path_provider_foundation: dependency: transitive description: name: path_provider_foundation url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.2.0" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.8" + version: "2.1.10" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_windows: dependency: transitive description: @@ -898,7 +898,7 @@ packages: name: percent_indicator url: "https://pub.dartlang.org" source: hosted - version: "4.2.2" + version: "4.2.3" petitparser: dependency: transitive description: @@ -919,7 +919,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.4" pool: dependency: transitive description: @@ -1024,7 +1024,7 @@ packages: name: rich_clipboard_windows url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" screen_retriever: dependency: transitive description: @@ -1038,49 +1038,49 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.17" + version: "2.0.20" shared_preferences_android: dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.15" + version: "2.0.17" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.5" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.6" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.5" shelf: dependency: transitive description: @@ -1122,7 +1122,7 @@ packages: name: sized_context url: "https://pub.dartlang.org" source: hosted - version: "1.0.0+1" + version: "1.0.0+4" sky_engine: dependency: transitive description: flutter @@ -1155,7 +1155,7 @@ packages: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.11" + version: "0.10.12" source_span: dependency: transitive description: @@ -1295,56 +1295,56 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.9" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.23" + version: "6.0.26" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.14" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "3.0.3" + version: "3.0.5" uuid: dependency: transitive description: @@ -1407,7 +1407,7 @@ packages: name: window_manager url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" xdg_directories: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index b79bcc5fbe..b9d5b4a875 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+1 +version: 0.1.1 environment: sdk: ">=2.18.0 <3.0.0" @@ -89,7 +89,7 @@ dependencies: google_fonts: ^3.0.1 file_picker: <=5.0.0 percent_indicator: ^4.0.1 - + appflowy_editor_plugins: path: packages/appflowy_editor_plugins calendar_view: ^1.0.1 diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index 88638912db..668338bfb5 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -73,7 +73,7 @@ class BoardTestContext { BoardTestContext(this.gridView, this._boardDataController); List get rowInfos { - return _boardDataController.rowInfos; + return _boardDataController.rowCache.rowInfos; } List get fieldContexts => fieldController.fieldInfos; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index c68dffb976..af85cc8bf2 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -26,7 +26,7 @@ class GridTestContext { GridTestContext(this.gridView, this.gridController); List get rowInfos { - return gridController.rowInfos; + return gridController.rowCache.rowInfos; } List get fieldContexts => fieldController.fieldInfos; diff --git a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart index 77311f395d..48afb2b614 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:appflowy/plugins/document/presentation/plugins/parsers/code_block_node_parser.dart'; import 'package:appflowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart'; import 'package:appflowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -31,7 +32,31 @@ void main() { ]); expect(result, r'$$E = MC^2$$'); }); - + // Changes + test('code block', () { + const text = ''' +{ + "document":{ + "type":"editor", + "children":[ + { + "type":"code_block", + "attributes":{ + "code_block":"Some Code" + } + } + ] + } +} +'''; + final document = Document.fromJson( + Map.from(json.decode(text)), + ); + final result = documentToMarkdown(document, customParsers: [ + const CodeBlockNodeParser(), + ]); + expect(result, '```\nSome Code\n```'); + }); test('divider', () { const text = ''' { diff --git a/frontend/appflowy_flutter/windows/CMakeLists.txt b/frontend/appflowy_flutter/windows/CMakeLists.txt index 066bfe709e..5be6e64915 100644 --- a/frontend/appflowy_flutter/windows/CMakeLists.txt +++ b/frontend/appflowy_flutter/windows/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) project(appflowy_flutter LANGUAGES CXX) -set(BINARY_NAME "appflowy_flutter") +set(BINARY_NAME "AppFlowy") cmake_policy(SET CMP0063 NEW) @@ -9,6 +9,7 @@ set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Configure build options. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) @@ -50,14 +51,15 @@ add_subdirectory("runner") # them to the application. include(flutter/generated_plugins.cmake) - # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") + # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) + if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() diff --git a/frontend/appflowy_flutter/windows/runner/CMakeLists.txt b/frontend/appflowy_flutter/windows/runner/CMakeLists.txt index df6e8b99b5..17411a8ab8 100644 --- a/frontend/appflowy_flutter/windows/runner/CMakeLists.txt +++ b/frontend/appflowy_flutter/windows/runner/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" @@ -10,13 +15,25 @@ add_executable(${BINARY_NAME} WIN32 "Runner.rc" "runner.exe.manifest" ) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) - - -# === Flutter Library === -#set(DART_FFI "${CMAKE_CURRENT_SOURCE_DIR}/dart_ffi/dart_ffi.dll") -#set(DART_FFI ${DART_FFI} PARENT_SCOPE) \ No newline at end of file diff --git a/frontend/appflowy_flutter/windows/runner/Runner.rc b/frontend/appflowy_flutter/windows/runner/Runner.rc index 1f8c964ed2..0639f41ec5 100644 --- a/frontend/appflowy_flutter/windows/runner/Runner.rc +++ b/frontend/appflowy_flutter/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif @@ -89,13 +89,13 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.example" "\0" + VALUE "CompanyName", "io.appflowy" "\0" VALUE "FileDescription", "AppFlowy" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "appflowy_flutter" "\0" - VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "appflowy_flutter.exe" "\0" - VALUE "ProductName", "appflowy_flutter" "\0" + VALUE "InternalName", "AppFlowy" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 io.appflowy. All rights reserved." "\0" + VALUE "OriginalFilename", "AppFlowy.exe" "\0" + VALUE "ProductName", "AppFlowy" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/frontend/appflowy_flutter/windows/runner/main.cpp b/frontend/appflowy_flutter/windows/runner/main.cpp index 8f38159b86..2f7c10b343 100644 --- a/frontend/appflowy_flutter/windows/runner/main.cpp +++ b/frontend/appflowy_flutter/windows/runner/main.cpp @@ -6,10 +6,12 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _In_ wchar_t *command_line, _In_ int show_command) +{ // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { CreateAndAttachConsole(); } @@ -27,13 +29,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"appflowy_flutter", origin, size)) { + if (!window.CreateAndShow(L"AppFlowy", origin, size)) + { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { + while (::GetMessage(&msg, nullptr, 0, 0)) + { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 4dd4a9f63b..6f9ba16df5 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -20,15 +20,21 @@ "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", "@reduxjs/toolkit": "^1.9.2", + "@slate-yjs/core": "^0.3.1", + "@tanstack/react-virtual": "3.0.0-beta.54", "@tauri-apps/api": "^1.2.0", + "dayjs": "^1.11.7", + "events": "^3.3.0", "google-protobuf": "^3.21.2", "i18next": "^22.4.10", "i18next-browser-languagedetector": "^7.0.1", "is-hotkey": "^0.2.0", "jest": "^29.5.0", "nanoid": "^4.0.0", + "protoc-gen-ts": "^0.8.5", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", + "react-calendar": "^4.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", "react-i18next": "^12.2.0", @@ -40,7 +46,9 @@ "slate": "^0.91.4", "slate-react": "^0.91.9", "ts-results": "^3.3.0", - "utf8": "^3.0.0" + "utf8": "^3.0.0", + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.51" }, "devDependencies": { "@tauri-apps/cli": "^1.2.2", @@ -51,6 +59,7 @@ "@types/react-beautiful-dnd": "^13.1.3", "@types/react-dom": "^18.0.6", "@types/utf8": "^3.0.1", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitejs/plugin-react": "^3.0.0", @@ -62,6 +71,7 @@ "prettier-plugin-tailwindcss": "^0.2.2", "tailwindcss": "^3.2.7", "typescript": "^4.6.4", + "uuid": "^9.0.0", "vite": "^4.0.0" } } diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index a6c00ac119..426fb22859 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -6,31 +6,34 @@ specifiers: '@mui/icons-material': ^5.11.11 '@mui/material': ^5.11.12 '@reduxjs/toolkit': ^1.9.2 + '@slate-yjs/core': ^0.3.1 + '@tanstack/react-virtual': 3.0.0-beta.54 '@tauri-apps/api': ^1.2.0 '@tauri-apps/cli': ^1.2.2 '@types/google-protobuf': ^3.15.6 '@types/is-hotkey': ^0.1.7 - '@types/jest': ^29.4.0 - '@types/mocha': ^10.0.1 '@types/node': ^18.7.10 '@types/react': ^18.0.15 '@types/react-dom': ^18.0.6 '@types/utf8': ^3.0.1 + '@types/uuid': ^9.0.1 '@typescript-eslint/eslint-plugin': ^5.51.0 '@typescript-eslint/parser': ^5.51.0 '@vitejs/plugin-react': ^3.0.0 autoprefixer: ^10.4.13 eslint: ^8.34.0 eslint-plugin-react: ^7.32.2 + events: ^3.3.0 google-protobuf: ^3.21.2 i18next: ^22.4.10 i18next-browser-languagedetector: ^7.0.1 is-hotkey: ^0.2.0 - jest: ^29.4.3 + jest: ^29.5.0 nanoid: ^4.0.0 postcss: ^8.4.21 prettier: 2.8.4 prettier-plugin-tailwindcss: ^0.2.2 + protoc-gen-ts: ^0.8.5 react: ^18.2.0 react-dom: ^18.2.0 react-error-boundary: ^3.1.4 @@ -43,11 +46,13 @@ specifiers: slate: ^0.91.4 slate-react: ^0.91.9 tailwindcss: ^3.2.7 - ts-jest: ^29.0.5 ts-results: ^3.3.0 typescript: ^4.6.4 utf8: ^3.0.0 + uuid: ^9.0.0 vite: ^4.0.0 + y-indexeddb: ^9.0.9 + yjs: ^13.5.51 dependencies: '@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34 @@ -55,13 +60,17 @@ dependencies: '@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi '@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli '@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq + '@slate-yjs/core': 0.3.1_slate@0.91.4+yjs@13.5.51 + '@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0 '@tauri-apps/api': 1.2.0 + events: 3.3.0 google-protobuf: 3.21.2 i18next: 22.4.10 i18next-browser-languagedetector: 7.0.1 is-hotkey: 0.2.0 - jest: 29.4.3_@types+node@18.14.6 + jest: 29.5.0_@types+node@18.14.6 nanoid: 4.0.1 + protoc-gen-ts: 0.8.6_ss7alqtodw6rv4lluxhr36xjoa react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-error-boundary: 3.1.4_react@18.2.0 @@ -75,17 +84,18 @@ dependencies: slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u ts-results: 3.3.0 utf8: 3.0.0 + y-indexeddb: 9.0.9_yjs@13.5.51 + yjs: 13.5.51 devDependencies: '@tauri-apps/cli': 1.2.3 '@types/google-protobuf': 3.15.6 '@types/is-hotkey': 0.1.7 - '@types/jest': 29.4.0 - '@types/mocha': 10.0.1 '@types/node': 18.14.6 '@types/react': 18.0.28 '@types/react-dom': 18.0.11 '@types/utf8': 3.0.1 + '@types/uuid': 9.0.1 '@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi '@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu '@vitejs/plugin-react': 3.1.0_vite@4.1.4 @@ -96,8 +106,8 @@ devDependencies: prettier: 2.8.4 prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4 tailwindcss: 3.2.7_postcss@8.4.21 - ts-jest: 29.0.5_orzzknleilowtsz34rkaotjvzm typescript: 4.9.5 + uuid: 9.0.0 vite: 4.1.4_@types+node@18.14.6 packages: @@ -261,6 +271,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} @@ -269,6 +280,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.21.0: resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -277,6 +289,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.21.0: resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} @@ -285,6 +298,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -293,6 +307,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.21.0: resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} @@ -302,6 +317,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.21.0: resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} @@ -310,6 +326,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -318,6 +335,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.21.0: resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -326,6 +344,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -334,6 +353,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -342,6 +362,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -350,6 +371,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.21.0: resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} @@ -359,6 +381,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-typescript/7.20.0_@babel+core@7.21.0: resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} @@ -368,6 +391,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-transform-react-jsx-self/7.21.0_@babel+core@7.21.0: resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} @@ -431,6 +455,7 @@ packages: /@bcoe/v8-coverage/0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: false /@emotion/babel-plugin/11.10.6: resolution: {integrity: sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==} @@ -797,24 +822,27 @@ packages: get-package-type: 0.1.0 js-yaml: 3.14.1 resolve-from: 5.0.0 + dev: false /@istanbuljs/schema/0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + dev: false - /@jest/console/29.4.3: - resolution: {integrity: sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A==} + /@jest/console/29.5.0: + resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-message-util: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 + dev: false - /@jest/core/29.4.3: - resolution: {integrity: sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ==} + /@jest/core/29.5.0: + resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -822,86 +850,92 @@ packages: node-notifier: optional: true dependencies: - '@jest/console': 29.4.3 - '@jest/reporters': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.10 - jest-changed-files: 29.4.3 - jest-config: 29.4.3_@types+node@18.14.6 - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 + jest-changed-files: 29.5.0 + jest-config: 29.5.0_@types+node@18.14.6 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-resolve-dependencies: 29.4.3 - jest-runner: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 - jest-watcher: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 micromatch: 4.0.5 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: - supports-color - ts-node + dev: false - /@jest/environment/29.4.3: - resolution: {integrity: sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA==} + /@jest/environment/29.5.0: + resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - jest-mock: 29.4.3 + jest-mock: 29.5.0 + dev: false - /@jest/expect-utils/29.4.3: - resolution: {integrity: sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ==} + /@jest/expect-utils/29.5.0: + resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 + dev: false - /@jest/expect/29.4.3: - resolution: {integrity: sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ==} + /@jest/expect/29.5.0: + resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.4.3 - jest-snapshot: 29.4.3 + expect: 29.5.0 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /@jest/fake-timers/29.4.3: - resolution: {integrity: sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw==} + /@jest/fake-timers/29.5.0: + resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@sinonjs/fake-timers': 10.0.2 '@types/node': 18.14.6 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 - jest-util: 29.4.3 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-util: 29.5.0 + dev: false - /@jest/globals/29.4.3: - resolution: {integrity: sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA==} + /@jest/globals/29.5.0: + resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/types': 29.4.3 - jest-mock: 29.4.3 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/types': 29.5.0 + jest-mock: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /@jest/reporters/29.4.3: - resolution: {integrity: sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg==} + /@jest/reporters/29.5.0: + resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -910,10 +944,10 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 '@types/node': 18.14.6 chalk: 4.1.2 @@ -926,21 +960,23 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + jest-worker: 29.5.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 v8-to-istanbul: 9.1.0 transitivePeerDependencies: - supports-color + dev: false /@jest/schemas/29.4.3: resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.25.24 + dev: false /@jest/source-map/29.4.3: resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==} @@ -949,49 +985,53 @@ packages: '@jridgewell/trace-mapping': 0.3.17 callsites: 3.1.0 graceful-fs: 4.2.10 + dev: false - /@jest/test-result/29.4.3: - resolution: {integrity: sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA==} + /@jest/test-result/29.5.0: + resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/types': 29.5.0 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 + dev: false - /@jest/test-sequencer/29.4.3: - resolution: {integrity: sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw==} + /@jest/test-sequencer/29.5.0: + resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.3 + '@jest/test-result': 29.5.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 + jest-haste-map: 29.5.0 slash: 3.0.0 + dev: false - /@jest/transform/29.4.3: - resolution: {integrity: sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg==} + /@jest/transform/29.5.0: + resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.21.0 - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 + jest-haste-map: 29.5.0 jest-regex-util: 29.4.3 - jest-util: 29.4.3 + jest-util: 29.5.0 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color + dev: false - /@jest/types/29.4.3: - resolution: {integrity: sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==} + /@jest/types/29.5.0: + resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.4.3 @@ -1000,6 +1040,7 @@ packages: '@types/node': 18.14.6 '@types/yargs': 17.0.22 chalk: 4.1.2 + dev: false /@jridgewell/gen-mapping/0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} @@ -1263,16 +1304,43 @@ packages: /@sinclair/typebox/0.25.24: resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + dev: false /@sinonjs/commons/2.0.0: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} dependencies: type-detect: 4.0.8 + dev: false /@sinonjs/fake-timers/10.0.2: resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} dependencies: '@sinonjs/commons': 2.0.0 + dev: false + + /@slate-yjs/core/0.3.1_slate@0.91.4+yjs@13.5.51: + resolution: {integrity: sha512-8nvS9m5FhMNONgydAfzwDCUhuoWbgzx5Bvw1/foSe+JO331UOT1xAKbUX5FzGCOunUcbRjMPXSdNyiPc0dodJg==} + peerDependencies: + slate: '>=0.70.0' + yjs: ^13.5.29 + dependencies: + slate: 0.91.4 + y-protocols: 1.0.5 + yjs: 13.5.51 + dev: false + + /@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0: + resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.0.0-beta.54 + react: 18.2.0 + dev: false + + /@tanstack/virtual-core/3.0.0-beta.54: + resolution: {integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==} + dev: false /@tauri-apps/api/1.2.0: resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==} @@ -1384,22 +1452,26 @@ packages: '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.18.3 + dev: false /@types/babel__generator/7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: '@babel/types': 7.21.2 + dev: false /@types/babel__template/7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: '@babel/parser': 7.21.2 '@babel/types': 7.21.2 + dev: false /@types/babel__traverse/7.18.3: resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} dependencies: '@babel/types': 7.21.2 + dev: false /@types/google-protobuf/3.15.6: resolution: {integrity: sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==} @@ -1409,6 +1481,7 @@ packages: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: '@types/node': 18.14.6 + dev: false /@types/hoist-non-react-statics/3.3.1: resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} @@ -1422,23 +1495,19 @@ packages: /@types/istanbul-lib-coverage/2.0.4: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: false /@types/istanbul-lib-report/3.0.0: resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} dependencies: '@types/istanbul-lib-coverage': 2.0.4 + dev: false /@types/istanbul-reports/3.0.1: resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} dependencies: '@types/istanbul-lib-report': 3.0.0 - - /@types/jest/29.4.0: - resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==} - dependencies: - expect: 29.4.3 - pretty-format: 29.4.3 - dev: true + dev: false /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} @@ -1448,10 +1517,6 @@ packages: resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: false - /@types/mocha/10.0.1: - resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==} - dev: true - /@types/node/18.14.6: resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==} @@ -1461,6 +1526,7 @@ packages: /@types/prettier/2.7.2: resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} + dev: false /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -1498,6 +1564,7 @@ packages: /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + dev: false /@types/use-sync-external-store/0.0.3: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} @@ -1507,13 +1574,19 @@ packages: resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==} dev: true + /@types/uuid/9.0.1: + resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} + dev: true + /@types/yargs-parser/21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: false /@types/yargs/17.0.22: resolution: {integrity: sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==} dependencies: '@types/yargs-parser': 21.0.0 + dev: false /@typescript-eslint/eslint-plugin/5.54.0_6mj2wypvdnknez7kws2nfdgupi: resolution: {integrity: sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw==} @@ -1708,6 +1781,7 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.21.3 + dev: false /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -1728,6 +1802,7 @@ packages: /ansi-styles/5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + dev: false /anymatch/3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} @@ -1744,6 +1819,7 @@ packages: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: sprintf-js: 1.0.3 + dev: false /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1806,22 +1882,23 @@ packages: engines: {node: '>= 0.4'} dev: true - /babel-jest/29.4.3_@babel+core@7.21.0: - resolution: {integrity: sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw==} + /babel-jest/29.5.0_@babel+core@7.21.0: + resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.21.0 - '@jest/transform': 29.4.3 + '@jest/transform': 29.5.0 '@types/babel__core': 7.20.0 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.4.3_@babel+core@7.21.0 + babel-preset-jest: 29.5.0_@babel+core@7.21.0 chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 transitivePeerDependencies: - supports-color + dev: false /babel-plugin-istanbul/6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} @@ -1834,15 +1911,17 @@ packages: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color + dev: false - /babel-plugin-jest-hoist/29.4.3: - resolution: {integrity: sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q==} + /babel-plugin-jest-hoist/29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.20.7 '@babel/types': 7.21.2 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.18.3 + dev: false /babel-plugin-macros/3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -1871,16 +1950,18 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.21.0 '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.21.0 '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.21.0 + dev: false - /babel-preset-jest/29.4.3_@babel+core@7.21.0: - resolution: {integrity: sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw==} + /babel-preset-jest/29.5.0_@babel+core@7.21.0: + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.21.0 - babel-plugin-jest-hoist: 29.4.3 + babel-plugin-jest-hoist: 29.5.0 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.21.0 + dev: false /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1912,20 +1993,15 @@ packages: node-releases: 2.0.10 update-browserslist-db: 1.0.10_browserslist@4.21.5 - /bs-logger/0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - dependencies: - fast-json-stable-stringify: 2.1.0 - dev: true - /bser/2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: node-int64: 0.4.0 + dev: false /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} @@ -1946,10 +2022,12 @@ packages: /camelcase/5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + dev: false /camelcase/6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + dev: false /caniuse-lite/1.0.30001460: resolution: {integrity: sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==} @@ -1972,6 +2050,7 @@ packages: /char-regex/1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + dev: false /chokidar/3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -1991,9 +2070,11 @@ packages: /ci-info/3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} + dev: false /cjs-module-lexer/1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + dev: false /cliui/8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -2002,6 +2083,7 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + dev: false /clsx/1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} @@ -2011,9 +2093,11 @@ packages: /co/4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: false /collect-v8-coverage/1.0.1: resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + dev: false /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2044,6 +2128,7 @@ packages: /convert-source-map/2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: false /cosmiconfig/7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} @@ -2086,6 +2171,7 @@ packages: /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: false /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2094,6 +2180,7 @@ packages: /deepmerge/4.3.0: resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==} engines: {node: '>=0.10.0'} + dev: false /define-properties/1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} @@ -2110,6 +2197,7 @@ packages: /detect-newline/3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + dev: false /detective/5.2.1: resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} @@ -2128,6 +2216,7 @@ packages: /diff-sequences/29.4.3: resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -2172,14 +2261,17 @@ packages: /emittery/0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + dev: false /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false /error-ex/1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 + dev: false /es-abstract/1.21.1: resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} @@ -2285,6 +2377,7 @@ packages: /escape-string-regexp/2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + dev: false /escape-string-regexp/4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} @@ -2412,6 +2505,7 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + dev: false /esquery/1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -2442,6 +2536,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /events/3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + /execa/5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2455,20 +2554,23 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 + dev: false /exit/0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + dev: false - /expect/29.4.3: - resolution: {integrity: sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg==} + /expect/29.5.0: + resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/expect-utils': 29.4.3 + '@jest/expect-utils': 29.5.0 jest-get-type: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + dev: false /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2502,6 +2604,7 @@ packages: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: bser: 2.1.1 + dev: false /file-entry-cache/6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} @@ -2526,6 +2629,7 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + dev: false /find-up/5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -2591,6 +2695,7 @@ packages: /get-caller-file/2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + dev: false /get-intrinsic/1.2.0: resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} @@ -2603,10 +2708,12 @@ packages: /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + dev: false /get-stream/6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + dev: false /get-symbol-description/1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} @@ -2682,6 +2789,7 @@ packages: /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: false /grapheme-splitter/1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -2736,6 +2844,7 @@ packages: /html-escaper/2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: false /html-parse-stringify/3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} @@ -2746,6 +2855,7 @@ packages: /human-signals/2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + dev: false /i18next-browser-languagedetector/7.0.1: resolution: {integrity: sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==} @@ -2782,6 +2892,7 @@ packages: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 + dev: false /imurmurhash/0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -2815,6 +2926,7 @@ packages: /is-arrayish/0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: false /is-bigint/1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -2862,10 +2974,12 @@ packages: /is-fullwidth-code-point/3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + dev: false /is-generator-fn/2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + dev: false /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -2925,6 +3039,7 @@ packages: /is-stream/2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + dev: false /is-string/1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} @@ -2960,9 +3075,14 @@ packages: /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isomorphic.js/0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + dev: false + /istanbul-lib-coverage/3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} + dev: false /istanbul-lib-instrument/5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} @@ -2975,6 +3095,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: false /istanbul-lib-report/3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} @@ -2983,6 +3104,7 @@ packages: istanbul-lib-coverage: 3.2.0 make-dir: 3.1.0 supports-color: 7.2.0 + dev: false /istanbul-lib-source-maps/4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} @@ -2993,6 +3115,7 @@ packages: source-map: 0.6.1 transitivePeerDependencies: - supports-color + dev: false /istanbul-reports/3.1.5: resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} @@ -3000,42 +3123,46 @@ packages: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.0 + dev: false - /jest-changed-files/29.4.3: - resolution: {integrity: sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ==} + /jest-changed-files/29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 p-limit: 3.1.0 + dev: false - /jest-circus/29.4.3: - resolution: {integrity: sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw==} + /jest-circus/29.5.0: + resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 - jest-each: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 + jest-each: 29.5.0 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 p-limit: 3.1.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + pure-rand: 6.0.1 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: - supports-color + dev: false - /jest-cli/29.4.3_@types+node@18.14.6: - resolution: {integrity: sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg==} + /jest-cli/29.5.0_@types+node@18.14.6: + resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -3044,25 +3171,26 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 + '@jest/core': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.4.3_@types+node@18.14.6 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-config: 29.5.0_@types+node@18.14.6 + jest-util: 29.5.0 + jest-validate: 29.5.0 prompts: 2.4.2 yargs: 17.7.1 transitivePeerDependencies: - '@types/node' - supports-color - ts-node + dev: false - /jest-config/29.4.3_@types+node@18.14.6: - resolution: {integrity: sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==} + /jest-config/29.5.0_@types+node@18.14.6: + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -3074,128 +3202,139 @@ packages: optional: true dependencies: '@babel/core': 7.21.0 - '@jest/test-sequencer': 29.4.3 - '@jest/types': 29.4.3 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - babel-jest: 29.4.3_@babel+core@7.21.0 + babel-jest: 29.5.0_@babel+core@7.21.0 chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.0 glob: 7.2.3 graceful-fs: 4.2.10 - jest-circus: 29.4.3 - jest-environment-node: 29.4.3 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 jest-get-type: 29.4.3 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runner: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color + dev: false - /jest-diff/29.4.3: - resolution: {integrity: sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA==} + /jest-diff/29.5.0: + resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 diff-sequences: 29.4.3 jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false /jest-docblock/29.4.3: resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 + dev: false - /jest-each/29.4.3: - resolution: {integrity: sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q==} + /jest-each/29.5.0: + resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 chalk: 4.1.2 jest-get-type: 29.4.3 - jest-util: 29.4.3 - pretty-format: 29.4.3 + jest-util: 29.5.0 + pretty-format: 29.5.0 + dev: false - /jest-environment-node/29.4.3: - resolution: {integrity: sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg==} + /jest-environment-node/29.5.0: + resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - jest-mock: 29.4.3 - jest-util: 29.4.3 + jest-mock: 29.5.0 + jest-util: 29.5.0 + dev: false /jest-get-type/29.4.3: resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false - /jest-haste-map/29.4.3: - resolution: {integrity: sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ==} + /jest-haste-map/29.5.0: + resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.6 '@types/node': 18.14.6 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 jest-regex-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 + jest-util: 29.5.0 + jest-worker: 29.5.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 + dev: false - /jest-leak-detector/29.4.3: - resolution: {integrity: sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA==} + /jest-leak-detector/29.5.0: + resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false - /jest-matcher-utils/29.4.3: - resolution: {integrity: sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg==} + /jest-matcher-utils/29.5.0: + resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - jest-diff: 29.4.3 + jest-diff: 29.5.0 jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false - /jest-message-util/29.4.3: - resolution: {integrity: sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw==} + /jest-message-util/29.5.0: + resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.18.6 - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 stack-utils: 2.0.6 + dev: false - /jest-mock/29.4.3: - resolution: {integrity: sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg==} + /jest-mock/29.5.0: + resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - jest-util: 29.4.3 + jest-util: 29.5.0 + dev: false - /jest-pnp-resolver/1.2.3_jest-resolve@29.4.3: + /jest-pnp-resolver/1.2.3_jest-resolve@29.5.0: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} peerDependencies: @@ -3204,94 +3343,100 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.4.3 + jest-resolve: 29.5.0 + dev: false /jest-regex-util/29.4.3: resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false - /jest-resolve-dependencies/29.4.3: - resolution: {integrity: sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw==} + /jest-resolve-dependencies/29.5.0: + resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-regex-util: 29.4.3 - jest-snapshot: 29.4.3 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /jest-resolve/29.4.3: - resolution: {integrity: sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw==} + /jest-resolve/29.5.0: + resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-pnp-resolver: 1.2.3_jest-resolve@29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-haste-map: 29.5.0 + jest-pnp-resolver: 1.2.3_jest-resolve@29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 resolve: 1.22.1 resolve.exports: 2.0.0 slash: 3.0.0 + dev: false - /jest-runner/29.4.3: - resolution: {integrity: sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA==} + /jest-runner/29.5.0: + resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.3 - '@jest/environment': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/environment': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.10 jest-docblock: 29.4.3 - jest-environment-node: 29.4.3 - jest-haste-map: 29.4.3 - jest-leak-detector: 29.4.3 - jest-message-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runtime: 29.4.3 - jest-util: 29.4.3 - jest-watcher: 29.4.3 - jest-worker: 29.4.3 + jest-environment-node: 29.5.0 + jest-haste-map: 29.5.0 + jest-leak-detector: 29.5.0 + jest-message-util: 29.5.0 + jest-resolve: 29.5.0 + jest-runtime: 29.5.0 + jest-util: 29.5.0 + jest-watcher: 29.5.0 + jest-worker: 29.5.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color + dev: false - /jest-runtime/29.4.3: - resolution: {integrity: sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q==} + /jest-runtime/29.5.0: + resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/globals': 29.4.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/globals': 29.5.0 '@jest/source-map': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.3 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 + jest-resolve: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color + dev: false - /jest-snapshot/29.4.3: - resolution: {integrity: sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw==} + /jest-snapshot/29.5.0: + resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.21.0 @@ -3300,73 +3445,77 @@ packages: '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.21.0 '@babel/traverse': 7.21.2 '@babel/types': 7.21.2 - '@jest/expect-utils': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/expect-utils': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/babel__traverse': 7.18.3 '@types/prettier': 2.7.2 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.21.0 chalk: 4.1.2 - expect: 29.4.3 + expect: 29.5.0 graceful-fs: 4.2.10 - jest-diff: 29.4.3 + jest-diff: 29.5.0 jest-get-type: 29.4.3 - jest-haste-map: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 natural-compare: 1.4.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 semver: 7.3.8 transitivePeerDependencies: - supports-color + dev: false - /jest-util/29.4.3: - resolution: {integrity: sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q==} + /jest-util/29.5.0: + resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.10 picomatch: 2.3.1 + dev: false - /jest-validate/29.4.3: - resolution: {integrity: sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw==} + /jest-validate/29.5.0: + resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 camelcase: 6.3.0 chalk: 4.1.2 jest-get-type: 29.4.3 leven: 3.1.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false - /jest-watcher/29.4.3: - resolution: {integrity: sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA==} + /jest-watcher/29.5.0: + resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.4.3 + jest-util: 29.5.0 string-length: 4.0.2 + dev: false - /jest-worker/29.4.3: - resolution: {integrity: sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==} + /jest-worker/29.5.0: + resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 18.14.6 - jest-util: 29.4.3 + jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 + dev: false - /jest/29.4.3_@types+node@18.14.6: - resolution: {integrity: sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==} + /jest/29.5.0_@types+node@18.14.6: + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -3375,14 +3524,15 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.3 - '@jest/types': 29.4.3 + '@jest/core': 29.5.0 + '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.4.3_@types+node@18.14.6 + jest-cli: 29.5.0_@types+node@18.14.6 transitivePeerDependencies: - '@types/node' - supports-color - ts-node + dev: false /js-sdsl/4.3.0: resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} @@ -3397,6 +3547,7 @@ packages: dependencies: argparse: 1.0.10 esprima: 4.0.1 + dev: false /js-yaml/4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -3412,6 +3563,7 @@ packages: /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: false /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3437,10 +3589,12 @@ packages: /kleur/3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + dev: false /leven/3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + dev: false /levn/0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -3450,6 +3604,14 @@ packages: type-check: 0.4.0 dev: true + /lib0/0.2.73: + resolution: {integrity: sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==} + engines: {node: '>=14'} + hasBin: true + dependencies: + isomorphic.js: 0.2.5 + dev: false + /lilconfig/2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -3457,12 +3619,14 @@ packages: /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: false /locate-path/5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} dependencies: p-locate: 4.1.0 + dev: false /locate-path/6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -3471,10 +3635,6 @@ packages: p-locate: 5.0.0 dev: true - /lodash.memoize/4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: true - /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -3512,18 +3672,17 @@ packages: engines: {node: '>=8'} dependencies: semver: 6.3.0 - - /make-error/1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true + dev: false /makeerror/1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: tmpl: 1.0.5 + dev: false /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -3540,6 +3699,7 @@ packages: /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + dev: false /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3574,6 +3734,7 @@ packages: /node-int64/0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: false /node-releases/2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} @@ -3592,6 +3753,7 @@ packages: engines: {node: '>=8'} dependencies: path-key: 3.1.1 + dev: false /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -3665,6 +3827,7 @@ packages: engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 + dev: false /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} @@ -3683,6 +3846,7 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 + dev: false /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -3695,6 +3859,7 @@ packages: engines: {node: '>=8'} dependencies: p-limit: 2.3.0 + dev: false /p-locate/5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} @@ -3706,6 +3871,7 @@ packages: /p-try/2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + dev: false /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -3721,6 +3887,7 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + dev: false /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -3756,12 +3923,14 @@ packages: /pirates/4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} + dev: false /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} dependencies: find-up: 4.1.0 + dev: false /postcss-import/14.1.0_postcss@8.4.21: resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} @@ -3899,13 +4068,14 @@ packages: hasBin: true dev: true - /pretty-format/29.4.3: - resolution: {integrity: sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA==} + /pretty-format/29.5.0: + resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.4.3 ansi-styles: 5.2.0 react-is: 18.2.0 + dev: false /prompts/2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} @@ -3913,6 +4083,7 @@ packages: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 + dev: false /prop-types/15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -3921,11 +4092,26 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /protoc-gen-ts/0.8.6_ss7alqtodw6rv4lluxhr36xjoa: + resolution: {integrity: sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==} + hasBin: true + peerDependencies: + google-protobuf: ^3.13.0 + typescript: 4.x.x + dependencies: + google-protobuf: 3.21.2 + typescript: 4.9.5 + dev: false + /punycode/2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} dev: true + /pure-rand/6.0.1: + resolution: {integrity: sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==} + dev: false + /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -3980,6 +4166,7 @@ packages: /react-is/18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false /react-redux/8.0.5_ctrls2ti7t7iutxbwkm5ipogyy: resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==} @@ -4122,6 +4309,7 @@ packages: /require-directory/2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + dev: false /reselect/4.1.7: resolution: {integrity: sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==} @@ -4132,6 +4320,7 @@ packages: engines: {node: '>=8'} dependencies: resolve-from: 5.0.0 + dev: false /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -4140,10 +4329,12 @@ packages: /resolve-from/5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + dev: false /resolve.exports/2.0.0: resolution: {integrity: sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==} engines: {node: '>=10'} + dev: false /resolve/1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} @@ -4245,9 +4436,11 @@ packages: /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -4292,6 +4485,7 @@ packages: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + dev: false /source-map/0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} @@ -4301,15 +4495,18 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + dev: false /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false /stack-utils/2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} dependencies: escape-string-regexp: 2.0.0 + dev: false /string-length/4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -4317,6 +4514,7 @@ packages: dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 + dev: false /string-width/4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -4325,6 +4523,7 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + dev: false /string.prototype.matchall/4.0.8: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} @@ -4364,10 +4563,12 @@ packages: /strip-bom/4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} + dev: false /strip-final-newline/2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + dev: false /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -4394,6 +4595,7 @@ packages: engines: {node: '>=10'} dependencies: has-flag: 4.0.0 + dev: false /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -4440,6 +4642,7 @@ packages: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 minimatch: 3.1.2 + dev: false /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4455,6 +4658,7 @@ packages: /tmpl/1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: false /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} @@ -4466,39 +4670,6 @@ packages: dependencies: is-number: 7.0.0 - /ts-jest/29.0.5_orzzknleilowtsz34rkaotjvzm: - resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.4.3_@types+node@18.14.6 - jest-util: 29.4.3 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.8 - typescript: 4.9.5 - yargs-parser: 21.1.1 - dev: true - /ts-results/3.3.0: resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==} dev: false @@ -4531,6 +4702,7 @@ packages: /type-detect/4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + dev: false /type-fest/0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} @@ -4540,6 +4712,7 @@ packages: /type-fest/0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + dev: false /typed-array-length/1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} @@ -4553,7 +4726,6 @@ packages: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} hasBin: true - dev: true /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -4596,6 +4768,11 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /uuid/9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: true + /v8-to-istanbul/9.1.0: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} @@ -4603,6 +4780,7 @@ packages: '@jridgewell/trace-mapping': 0.3.17 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 + dev: false /vite/4.1.4_@types+node@18.14.6: resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} @@ -4647,6 +4825,7 @@ packages: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: makeerror: 1.0.12 + dev: false /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -4689,6 +4868,7 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + dev: false /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4699,15 +4879,32 @@ packages: dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 + dev: false /xtend/4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} dev: true + /y-indexeddb/9.0.9_yjs@13.5.51: + resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.73 + yjs: 13.5.51 + dev: false + + /y-protocols/1.0.5: + resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==} + dependencies: + lib0: 0.2.73 + dev: false + /y18n/5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + dev: false /yallist/3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4722,6 +4919,7 @@ packages: /yargs-parser/21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + dev: false /yargs/17.7.1: resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} @@ -4734,6 +4932,14 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 + dev: false + + /yjs/13.5.51: + resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + dependencies: + lib0: 0.2.73 + dev: false /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts deleted file mode 100644 index 8fb8610b77..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BlockInterface, BlockType } from '$app/interfaces/index'; - - -export class BlockDataManager { - private head: BlockInterface | null = null; - constructor(id: string, private map: Record> | null) { - if (!map) return; - this.head = map[id]; - } - - setBlocksMap = (id: string, map: Record>) => { - this.map = map; - this.head = map[id]; - } - - /** - * get block data - * @param blockId string - * @returns Block - */ - getBlock = (blockId: string) => { - return this.map?.[blockId] || null; - } - - destroy() { - this.map = null; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts deleted file mode 100644 index 01f49f656b..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { BlockInterface } from '../interfaces'; -import { BlockDataManager } from './block'; -import { TreeManager } from './tree'; - -/** - * BlockEditor is a document data manager that operates on and renders data through managing blockData and RenderTreeManager. - * The render tree will be re-render and update react component when block makes changes to the data. - * RectManager updates the cache of node rect when the react component update is completed. - */ -export class BlockEditor { - // blockData manages document block data, including operations such as add, delete, update, and move. - public blockData: BlockDataManager; - // RenderTreeManager manages data rendering, including the construction and updating of the render tree. - public renderTree: TreeManager; - - constructor(private id: string, data: Record) { - this.blockData = new BlockDataManager(id, data); - this.renderTree = new TreeManager(this.blockData.getBlock); - } - - /** - * update id and map when the doc is change - * @param id - * @param data - */ - changeDoc = (id: string, data: Record) => { - console.log('==== change document ====', id, data) - this.id = id; - this.blockData.setBlocksMap(id, data); - } - - destroy = () => { - this.renderTree.destroy(); - this.blockData.destroy(); - } - -} - -let blockEditorInstance: BlockEditor | null; - -export function getBlockEditor() { - return blockEditorInstance; -} - -export function createBlockEditor(id: string, data: Record) { - blockEditorInstance = new BlockEditor(id, data); - return blockEditorInstance; -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts deleted file mode 100644 index 5398ab4a6f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TreeNodeInterface } from "../interfaces"; - - -export function calculateBlockRect(blockId: string) { - const el = document.querySelectorAll(`[data-block-id=${blockId}]`)[0]; - return el?.getBoundingClientRect(); -} - -export class RectManager { - map: Map; - - orderList: Set; - - private updatedQueue: Set; - - constructor(private getTreeNode: (nodeId: string) => TreeNodeInterface | null) { - this.map = new Map(); - this.orderList = new Set(); - this.updatedQueue = new Set(); - } - - build() { - console.log('====update all blocks position====') - this.orderList.forEach(id => this.updateNodeRect(id)); - } - - getNodeRect = (nodeId: string) => { - return this.map.get(nodeId) || null; - } - - update() { - // In order to avoid excessive calculation frequency - // calculate and update the block position information in the queue every frame - requestAnimationFrame(() => { - // there is nothing to do if the updated queue is empty - if (this.updatedQueue.size === 0) return; - console.log(`==== update ${this.updatedQueue.size} blocks rect cache ====`) - this.updatedQueue.forEach((id: string) => { - const rect = calculateBlockRect(id); - this.map.set(id, rect); - this.updatedQueue.delete(id); - }); - }); - } - - updateNodeRect = (nodeId: string) => { - if (this.updatedQueue.has(nodeId)) return; - let node: TreeNodeInterface | null = this.getTreeNode(nodeId); - - // When one of the blocks is updated - // the positions of all its parent and child blocks need to be updated - while(node) { - node.parent?.children.forEach(child => this.updatedQueue.add(child.id)); - node = node.parent; - } - - this.update(); - } - - destroy() { - this.map.clear(); - this.orderList.clear(); - this.updatedQueue.clear(); - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts deleted file mode 100644 index bc545139fc..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { RectManager } from "./rect"; -import { BlockInterface, BlockData, BlockType, TreeNodeInterface } from '../interfaces/index'; - -export class TreeManager { - - // RenderTreeManager holds RectManager, which manages the position information of each node in the render tree. - private rect: RectManager; - - root: TreeNode | null = null; - - map: Map = new Map(); - - constructor(private getBlock: (blockId: string) => BlockInterface | null) { - this.rect = new RectManager(this.getTreeNode); - } - - /** - * Get render node data by nodeId - * @param nodeId string - * @returns TreeNode - */ - getTreeNode = (nodeId: string): TreeNodeInterface | null => { - return this.map.get(nodeId) || null; - } - - /** - * build tree node for rendering - * @param rootId - * @returns - */ - build(rootId: string): TreeNode | null { - const head = this.getBlock(rootId); - - if (!head) return null; - - this.root = new TreeNode(head); - - let node = this.root; - - // loop line - while (node) { - this.map.set(node.id, node); - this.rect.orderList.add(node.id); - - const block = this.getBlock(node.id)!; - const next = block.next ? this.getBlock(block.next) : null; - const firstChild = block.firstChild ? this.getBlock(block.firstChild) : null; - - // find next line - if (firstChild) { - // the next line is node's first child - const child = new TreeNode(firstChild); - node.addChild(child); - node = child; - } else if (next) { - // the next line is node's sibling - const sibling = new TreeNode(next); - node.parent?.addChild(sibling); - node = sibling; - } else { - // the next line is parent's sibling - let isFind = false; - while(node.parent) { - const parentId = node.parent.id; - const parent = this.getBlock(parentId)!; - const parentNext = parent.next ? this.getBlock(parent.next) : null; - if (parentNext) { - const parentSibling = new TreeNode(parentNext); - node.parent?.parent?.addChild(parentSibling); - node = parentSibling; - isFind = true; - break; - } else { - node = node.parent; - } - } - - if (!isFind) { - // Exit if next line not found - break; - } - - } - } - - return this.root; - } - - /** - * update dom rects cache - */ - updateRects = () => { - this.rect.build(); - } - - /** - * get block rect cache - * @param id string - * @returns DOMRect - */ - getNodeRect = (nodeId: string) => { - return this.rect.getNodeRect(nodeId); - } - - /** - * update block rect cache - * @param id string - */ - updateNodeRect = (nodeId: string) => { - this.rect.updateNodeRect(nodeId); - } - - destroy() { - this.rect?.destroy(); - } -} - - -class TreeNode implements TreeNodeInterface { - id: string; - type: BlockType; - parent: TreeNode | null = null; - children: TreeNode[] = []; - data: BlockData; - - constructor({ - id, - type, - data - }: BlockInterface) { - this.id = id; - this.data = data; - this.type = type; - } - - addChild(node: TreeNode) { - node.parent = this; - this.children.push(node); - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx deleted file mode 100644 index cbe27de694..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useSlate } from 'slate-react'; -import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format'; -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; -import { useMemo } from 'react'; -import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material'; -import { command, iconSize } from '$app/constants/toolbar'; - -const FormatButton = ({ format, icon }: { format: string; icon: string }) => { - const editor = useSlate(); - - const renderComponent = useMemo(() => { - switch (icon) { - case 'bold': - return ; - case 'underlined': - return ; - case 'italic': - return ; - case 'code': - return ; - case 'strikethrough': - return ; - default: - break; - } - }, [icon]); - - return ( - - {command[format].title} - {command[format].key} - - } - placement='top-start' - > - toggleFormat(editor, format)} - > - {renderComponent} - - - ); -}; - -export default FormatButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx deleted file mode 100644 index 0176c8f429..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/Portal.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ReactDOM from 'react-dom'; - -const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => { - const root = document.querySelectorAll(`[data-block-id=${blockId}] > .block-overlay`)[0]; - - return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null; -}; - -export default Portal; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx deleted file mode 100644 index 0a18c3f5e9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ReactDOM from 'react-dom'; -export const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => { - const root = document.querySelectorAll(`[data-block-id=${blockId}]`)[0]; - return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx deleted file mode 100644 index 7b8454800b..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { useFocused, useSlate } from 'slate-react'; -import FormatButton from './FormatButton'; -import Portal from './Portal'; -import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; - -const HoveringToolbar = ({ blockId }: { blockId: string }) => { - const editor = useSlate(); - const inFocus = useFocused(); - const ref = useRef(null); - - useEffect(() => { - const el = ref.current; - if (!el) return; - - const position = calcToolbarPosition(editor, el, blockId); - - if (!position) { - el.style.opacity = '0'; - } else { - el.style.opacity = '1'; - el.style.top = position.top; - el.style.left = position.left; - } - }); - - if (!inFocus) return null; - - return ( - -
{ - // prevent toolbar from taking focus away from editor - e.preventDefault(); - }} - > - {['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => ( - - ))} -
-
- ); -}; - -export default HoveringToolbar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx new file mode 100644 index 0000000000..63aeeab62a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx @@ -0,0 +1,34 @@ +import { SelectOptionCellDataPB } from '@/services/backend'; +import { getBgColor } from '$app/components/_shared/getColor'; +import { useRef } from 'react'; + +export const CellOptions = ({ + data, + onEditClick, +}: { + data: SelectOptionCellDataPB | undefined; + onEditClick: (left: number, top: number) => void; +}) => { + const ref = useRef(null); + + const onClick = () => { + if (!ref.current) return; + const { left, top } = ref.current.getBoundingClientRect(); + onEditClick(left, top); + }; + + return ( +
onClick()} + className={'flex flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'} + > + {data?.select_options?.map((option, index) => ( +
+ {option?.name || ''} +
+ )) || ''} +   +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx new file mode 100644 index 0000000000..0a2b1cf09b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx @@ -0,0 +1,152 @@ +import { KeyboardEventHandler, useEffect, useRef, useState } from 'react'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { useCell } from '$app/components/_shared/database-hooks/useCell'; +import { CellCache } from '$app/stores/effects/database/cell/cell_cache'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { SelectOptionCellDataPB, SelectOptionColorPB, SelectOptionPB } from '@/services/backend'; +import { getBgColor } from '$app/components/_shared/getColor'; +import { useTranslation } from 'react-i18next'; +import { Details2Svg } from '$app/components/_shared/svg/Details2Svg'; +import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg'; +import { CloseSvg } from '$app/components/_shared/svg/CloseSvg'; +import useOutsideClick from '$app/components/_shared/useOutsideClick'; +import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc'; +import { useAppSelector } from '$app/stores/store'; +import { ISelectOptionType } from '$app/stores/reducers/database/slice'; + +export const CellOptionsPopup = ({ + top, + left, + cellIdentifier, + cellCache, + fieldController, + onOutsideClick, +}: { + top: number; + left: number; + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const ref = useRef(null); + const { t } = useTranslation(''); + const [adjustedTop, setAdjustedTop] = useState(-100); + const [value, setValue] = useState(''); + const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); + const databaseStore = useAppSelector((state) => state.database); + + useEffect(() => { + if (!ref.current) return; + const { height } = ref.current.getBoundingClientRect(); + if (top + height + 40 > window.innerHeight) { + setAdjustedTop(window.innerHeight - height - 40); + } else { + setAdjustedTop(top); + } + }, [ref, window, top, left]); + + useOutsideClick(ref, async () => { + onOutsideClick(); + }); + + const onKeyDown: KeyboardEventHandler = async (e) => { + if (e.key === 'Enter' && value.length > 0) { + await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value }); + setValue(''); + } + }; + + const onUnselectOptionClick = async (option: SelectOptionPB) => { + await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]); + setValue(''); + }; + + const onToggleOptionClick = async (option: SelectOptionPB) => { + if ( + (data as SelectOptionCellDataPB | undefined)?.select_options?.find( + (selectedOption) => selectedOption.id === option.id + ) + ) { + await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]); + } else { + await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]); + } + setValue(''); + }; + + useEffect(() => { + console.log('loaded data: ', data); + console.log('have stored ', databaseStore.fields[cellIdentifier.fieldId]); + }, [data]); + + return ( +
+
+
+
+ {(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => ( +
+ {option?.name || ''} + +
+ )) || ''} +
+ setValue(e.target.value)} + placeholder={t('grid.selectOption.searchOption') || ''} + onKeyDown={onKeyDown} + /> +
{value.length}/30
+
+
+
{t('grid.selectOption.panelTitle') || ''}
+
+ {(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map( + (option, index) => ( +
+ onToggleOptionClick( + new SelectOptionPB({ + id: option.selectOptionId, + name: option.title, + color: option.color || SelectOptionColorPB.Purple, + }) + ) + } + className={ + 'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary' + } + > +
{option.title}
+
+ {(data as SelectOptionCellDataPB | undefined)?.select_options?.find( + (selectedOption) => selectedOption.id === option.selectOptionId + ) && ( + + )} + +
+
+ ) + )} +
+
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx new file mode 100644 index 0000000000..da7e8b0375 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx @@ -0,0 +1,71 @@ +import { FieldType } from '@/services/backend'; +import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon'; +import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import useOutsideClick from '$app/components/_shared/useOutsideClick'; + +const typesOrder: FieldType[] = [ + FieldType.RichText, + FieldType.Number, + FieldType.DateTime, + FieldType.SingleSelect, + FieldType.MultiSelect, + FieldType.Checkbox, + FieldType.URL, + FieldType.Checklist, +]; + +export const ChangeFieldTypePopup = ({ + top, + right, + onClick, + onOutsideClick, +}: { + top: number; + right: number; + onClick: (newType: FieldType) => void; + onOutsideClick: () => void; +}) => { + const ref = useRef(null); + const [adjustedTop, setAdjustedTop] = useState(-100); + useOutsideClick(ref, async () => { + onOutsideClick(); + }); + + useEffect(() => { + if (!ref.current) return; + const { height } = ref.current.getBoundingClientRect(); + if (top + height > window.innerHeight) { + setAdjustedTop(window.innerHeight - height); + } else { + setAdjustedTop(top); + } + }, [ref, window, top, right]); + + return ( +
+
+ {typesOrder.map((t, i) => ( + + ))} +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx new file mode 100644 index 0000000000..d7af34ec23 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx @@ -0,0 +1,97 @@ +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { CellCache } from '$app/stores/effects/database/cell/cell_cache'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import useOutsideClick from '$app/components/_shared/useOutsideClick'; +import Calendar from 'react-calendar'; +import dayjs from 'dayjs'; +import { ClockSvg } from '$app/components/_shared/svg/ClockSvg'; +import { MoreSvg } from '$app/components/_shared/svg/MoreSvg'; +import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg'; +import { useCell } from '$app/components/_shared/database-hooks/useCell'; + +export const DatePickerPopup = ({ + left, + top, + cellIdentifier, + cellCache, + fieldController, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); + const ref = useRef(null); + const [adjustedTop, setAdjustedTop] = useState(-100); + // const [value, setValue] = useState(); + const { t } = useTranslation(''); + const [selectedDate, setSelectedDate] = useState(new Date()); + + useEffect(() => { + if (!ref.current) return; + const { height } = ref.current.getBoundingClientRect(); + if (top + height + 40 > window.innerHeight) { + setAdjustedTop(top - height - 40); + } else { + setAdjustedTop(top); + } + }, [ref, window, top, left]); + + useOutsideClick(ref, async () => { + onOutsideClick(); + }); + + useEffect(() => { + // console.log((data as DateCellDataPB).date); + // setSelectedDate(new Date((data as DateCellDataPB).date)); + }, [data]); + + const onChange = (v: Date | null | (Date | null)[]) => { + if (v instanceof Date) { + console.log(dayjs(v).format('YYYY-MM-DD')); + setSelectedDate(v); + // void cellController?.saveCellData(new DateCellDataPB({ date: dayjs(v).format('YYYY-MM-DD') })); + } + }; + + return ( +
+
+ onChange(d)} value={selectedDate} /> +
+
+
+
+ + + + {t('grid.field.includeTime')} +
+ + + +
+
+
+ + {t('grid.field.dateFormat')} & {t('grid.field.timeFormat')} + + + + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx new file mode 100644 index 0000000000..faf24eaf3a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx @@ -0,0 +1,24 @@ +import { useRef } from 'react'; +import { DateCellDataPB } from '@/services/backend'; + +export const EditCellDate = ({ + data, + onEditClick, +}: { + data?: DateCellDataPB; + onEditClick: (left: number, top: number) => void; +}) => { + const ref = useRef(null); + + const onClick = () => { + if (!ref.current) return; + const { left, top } = ref.current.getBoundingClientRect(); + onEditClick(left, top); + }; + + return ( +
onClick()} className={'px-4 py-2'}> + {data?.date || <> } +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx new file mode 100644 index 0000000000..205ddd9257 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx @@ -0,0 +1,29 @@ +import { CellController } from '$app/stores/effects/database/cell/cell_controller'; +import { useEffect, useState } from 'react'; + +export const EditCellNumber = ({ + data, + cellController, +}: { + data: string | undefined; + cellController: CellController; +}) => { + const [value, setValue] = useState(''); + + useEffect(() => { + setValue(data || ''); + }, [data]); + + const save = async () => { + await cellController?.saveCellData(value); + }; + + return ( + setValue(e.target.value)} + onBlur={() => save()} + className={'w-full px-4 py-2'} + > + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx index bc36f35ecf..65c09e9880 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx @@ -1,11 +1,18 @@ import { CellController } from '$app/stores/effects/database/cell/cell_controller'; import { useEffect, useState, KeyboardEvent, useMemo } from 'react'; -export const EditCellText = ({ data, cellController }: { data: string; cellController: CellController }) => { +export const EditCellText = ({ + data, + cellController, +}: { + data: string | undefined; + cellController: CellController; +}) => { const [value, setValue] = useState(''); const [contentRows, setContentRows] = useState(1); + useEffect(() => { - setValue(data); + setValue(data || ''); }, [data]); useEffect(() => { @@ -21,12 +28,14 @@ export const EditCellText = ({ data, cellController }: { data: string; cellContr }; return ( -