diff --git a/.github/workflows/appflowy_editor_test.yml b/.github/workflows/appflowy_editor_test.yml index 80b9337f7b..c007259bf8 100644 --- a/.github/workflows/appflowy_editor_test.yml +++ b/.github/workflows/appflowy_editor_test.yml @@ -4,8 +4,6 @@ on: push: branches: - "main" - paths: - - "frontend/app_flowy/packages/appflowy_editor/**" pull_request: branches: diff --git a/.github/workflows/dart_test.yml b/.github/workflows/dart_test.yml index 902a861239..17694e3fec 100644 --- a/.github/workflows/dart_test.yml +++ b/.github/workflows/dart_test.yml @@ -67,7 +67,7 @@ jobs: - name: Build FlowySDK working-directory: frontend run: | - cargo make --profile development-linux-x86_64 flowy-sdk-dev + cargo make --profile test-linux test-lib-build - name: Code Generation working-directory: frontend/app_flowy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9c1cb00f8..d7e22b4b46 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,7 +118,7 @@ jobs: - name: Archive macOS app working-directory: ${{ env.MACOS_APP_RELEASE_PATH }} - run: zip -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app + run: zip --symlinks -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app - name: Upload Release Asset uses: actions/upload-release-asset@v1 diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 3e73dd7a36..332a8e72db 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -40,10 +40,15 @@ PRODUCT_NAME = "AppFlowy" # for cdylib: # if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.dylib'); CRATE_TYPE = "staticlib" -SDK_EXT = "a" +LIB_EXT = "a" APP_ENVIRONMENT = "local" FLUTTER_FLOWY_SDK_PATH = "app_flowy/packages/flowy_sdk" PROTOBUF_DERIVE_CACHE = "../shared-lib/flowy-derive/src/derive_cache/derive_cache.rs" +# Test default config +TEST_CRATE_TYPE = "cdylib" +TEST_LIB_EXT = "dylib" +TEST_BUILD_FLAG = "debug" +TEST_COMPILE_TARGET = "x86_64-apple-darwin" [env.development-mac-arm64] RUST_LOG = "info" @@ -88,7 +93,7 @@ BUILD_FLAG = "debug" FLUTTER_OUTPUT_DIR = "Debug" PRODUCT_EXT = "exe" CRATE_TYPE = "cdylib" -SDK_EXT = "dll" +LIB_EXT = "dll" [env.production-windows-x86] BUILD_FLAG = "release" @@ -97,7 +102,7 @@ RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc" FLUTTER_OUTPUT_DIR = "Release" PRODUCT_EXT = "exe" CRATE_TYPE = "cdylib" -SDK_EXT = "dll" +LIB_EXT = "dll" APP_ENVIRONMENT = "production" [env.development-linux-x86_64] @@ -106,7 +111,7 @@ RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu" BUILD_FLAG = "debug" CRATE_TYPE = "cdylib" FLUTTER_OUTPUT_DIR = "Debug" -SDK_EXT = "so" +LIB_EXT = "so" LINUX_ARCH = "x64" [env.production-linux-x86_64] @@ -115,7 +120,7 @@ TARGET_OS = "linux" RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu" CRATE_TYPE = "cdylib" FLUTTER_OUTPUT_DIR = "Release" -SDK_EXT = "so" +LIB_EXT = "so" LINUX_ARCH = "x64" APP_ENVIRONMENT = "production" @@ -125,7 +130,7 @@ RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu" BUILD_FLAG = "debug" CRATE_TYPE = "cdylib" FLUTTER_OUTPUT_DIR = "Debug" -SDK_EXT = "so" +LIB_EXT = "so" LINUX_ARCH = "arm64" [env.production-linux-aarch64] @@ -134,7 +139,7 @@ TARGET_OS = "linux" RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu" CRATE_TYPE = "cdylib" FLUTTER_OUTPUT_DIR = "Release" -SDK_EXT = "so" +LIB_EXT = "so" LINUX_ARCH = "arm64" APP_ENVIRONMENT = "production" @@ -197,6 +202,46 @@ script = [ ] script_runner = "@duckscript" +[env.test-macos] +TEST_CRATE_TYPE = "cdylib" +TEST_LIB_EXT = "dylib" +# For the moment, the DynamicLibrary only supports open x86_64 architectures binary. +TEST_COMPILE_TARGET = "x86_64-apple-darwin" + +[env.test-linux] +TEST_CRATE_TYPE = "cdylib" +TEST_LIB_EXT = "so" +TEST_COMPILE_TARGET = "x86_64-unknown-linux-gnu" + +[env.test-windows] +TEST_CRATE_TYPE = "cdylib" +TEST_LIB_EXT = "dll" +TEST_COMPILE_TARGET = "x86_64-pc-windows-msvc" + +[tasks.setup-test-crate-type] +private = true +script = [ + """ + toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml + val = replace ${toml} "staticlib" ${TEST_CRATE_TYPE} + result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} + assert ${result} + """, +] +script_runner = "@duckscript" + +[tasks.restore-test-crate-type] +private = true +script = [ + """ + toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml + val = replace ${toml} ${TEST_CRATE_TYPE} "staticlib" + result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} + assert ${result} + """, +] +script_runner = "@duckscript" + [tasks.test-build] condition = { env_set = ["FLUTTER_FLOWY_SDK_PATH"] } script = [""" @@ -204,3 +249,5 @@ script = [""" cargo build -vv --features=dart """] script_runner = "@shell" + + diff --git a/frontend/app_flowy/assets/translations/es-VE.json b/frontend/app_flowy/assets/translations/es-VE.json index 869a5e75a3..f928c18cda 100644 --- a/frontend/app_flowy/assets/translations/es-VE.json +++ b/frontend/app_flowy/assets/translations/es-VE.json @@ -7,7 +7,7 @@ "letsGoButtonText": "Vamos", "title": "Título", "signUp": { - "buttonText": "Registar", + "buttonText": "Registrar", "title": "Registrar en @:appName", "getStartedText": "Empezar", "emptyPasswordError": "La contraseña no puede estar en blanco", @@ -16,7 +16,7 @@ "alreadyHaveAnAccount": "¿Posee credenciales?", "emailHint": "Correo", "passwordHint": "Contraseña", - "repeatPasswordHint": "Repite la contraseña" + "repeatPasswordHint": "Repetir contraseña" }, "signIn": { "loginTitle": "Ingresa a @:appName", @@ -58,7 +58,7 @@ } }, "deletePagePrompt": { - "text": "Esta paágina esta en la Papelera", + "text": "Esta página está en la Papelera", "restore": "Recuperar página", "deletePermanent": "Eliminar permanentemente" }, @@ -69,7 +69,7 @@ "debug": { "name": "Información de depuración", "success": "¡Información copiada!", - "fail": "No fué posible copiar la información" + "fail": "No fue posible copiar la información" } }, "menuAppHeader": { @@ -167,15 +167,15 @@ "singleSelectFieldName": "Seleccionar", "multiSelectFieldName": "Selección múltiple", "urlFieldName": "URL", - "numberFormat": " Formato numérico", - "dateFormat": " Formato de fecha", - "includeTime": " Incluir tiempo", + "numberFormat": "Formato numérico", + "dateFormat": "Formato de fecha", + "includeTime": "Incluir tiempo", "dateFormatFriendly": "Mes Día, Año", "dateFormatISO": "Año-Mes-Día", "dateFormatLocal": "Mes/Día/Año", "dateFormatUS": "Año/Mes/Día", - "timeFormat": " Time format", - "invalidTimeFormat": "Formato de tiempo", + "timeFormat": "Formato de tiempo", + "invalidTimeFormat": "Formato de tiempo inválido", "timeFormatTwelveHour": "12 horas", "timeFormatTwentyFourHour": "24 horas", "addSelectOption": "Añadir una opción", @@ -205,17 +205,22 @@ "panelTitle": "Selecciona una opción o crea una", "searchOption": "Buscar una opción" }, - "menuName": "Grid" + "menuName": "Cuadrícula" }, "document": { - "menuName": "Doc", + "menuName": "Documento", "date": { "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" } }, "sideBar": { - "openSidebar": "Open sidebar", - "closeSidebar": "Close sidebar" + "openSidebar": "Abrir panel lateral", + "closeSidebar": "Cerrar panel lateral" + }, + "board": { + "column": { + "create_new_card": "Nuevo" + } } -} \ No newline at end of file +} diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index a53739f00a..e22677b9eb 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -27,7 +27,7 @@ class BoardPluginBuilder implements PluginBuilder { ViewDataTypePB get dataType => ViewDataTypePB.Database; @override - ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Board; + ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board; } class BoardPluginConfig implements PluginConfig { diff --git a/frontend/app_flowy/lib/plugins/board/tests/integrate_test/card_test.dart b/frontend/app_flowy/lib/plugins/board/tests/integrate_test/card_test.dart new file mode 100644 index 0000000000..fa267744c7 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/tests/integrate_test/card_test.dart @@ -0,0 +1,16 @@ +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:integration_test/integration_test.dart'; +// import 'package:app_flowy/main.dart' as app; + +// void main() { +// IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + +// group('end-to-end test', () { +// testWidgets('tap on the floating action button, verify counter', +// (tester) async { +// app.main(); + +// await tester.pumpAndSettle(); +// }); +// }); +// } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart index 476b7cdd40..d11a35e764 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart @@ -80,7 +80,12 @@ class TypeOptionDataController { Future switchToField(FieldType newFieldType) { return loader.switchToField(field.id, newFieldType).then((result) { return result.fold( - (_) {}, + (_) { + // Should load the type-option data after switching to a new field. + // After loading the type-option data, the editor widget that uses + // the type-option data will be rebuild. + loadTypeOptionData(); + }, (err) => Log.error(err), ); }); diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart index 0d679c831f..597e686b69 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart @@ -12,10 +12,12 @@ import 'grid_data_controller.dart'; import 'row/row_cache.dart'; import 'dart:collection'; +import 'row/row_service.dart'; part 'grid_bloc.freezed.dart'; class GridBloc extends Bloc { final GridDataController dataController; + void Function()? _createRowOperation; GridBloc({required ViewPB view}) : dataController = GridDataController(view: view), @@ -28,7 +30,19 @@ class GridBloc extends Bloc { await _loadGrid(emit); }, createRow: () { - dataController.createRow(); + state.loadingState.when( + loading: () { + _createRowOperation = () => dataController.createRow(); + }, + finish: (_) => dataController.createRow(), + ); + }, + deleteRow: (rowInfo) async { + final rowService = RowFFIService( + blockId: rowInfo.rowPB.blockId, + gridId: rowInfo.gridId, + ); + await rowService.deleteRow(rowInfo.rowPB.id); }, didReceiveGridUpdate: (grid) { emit(state.copyWith(grid: Some(grid))); @@ -84,9 +98,15 @@ class GridBloc extends Bloc { Future _loadGrid(Emitter emit) async { final result = await dataController.loadData(); result.fold( - (grid) => emit( - state.copyWith(loadingState: GridLoadingState.finish(left(unit))), - ), + (grid) { + if (_createRowOperation != null) { + _createRowOperation?.call(); + _createRowOperation = null; + } + emit( + state.copyWith(loadingState: GridLoadingState.finish(left(unit))), + ); + }, (err) => emit( state.copyWith(loadingState: GridLoadingState.finish(right(err))), ), @@ -98,6 +118,7 @@ class GridBloc extends Bloc { class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; const factory GridEvent.createRow() = _CreateRow; + const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow; const factory GridEvent.didReceiveRowUpdate( List rows, RowsChangedReason listState, diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index a8e1a27c39..aafaeb3774 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -64,6 +64,7 @@ class GridDataController { }); } + // Loads the rows from each block Future> loadData() async { final result = await _gridFFIService.loadGrid(); return Future( diff --git a/frontend/app_flowy/lib/plugins/grid/grid.dart b/frontend/app_flowy/lib/plugins/grid/grid.dart index 5d7f4b3bdb..91f6ca5063 100644 --- a/frontend/app_flowy/lib/plugins/grid/grid.dart +++ b/frontend/app_flowy/lib/plugins/grid/grid.dart @@ -29,7 +29,7 @@ class GridPluginBuilder implements PluginBuilder { ViewDataTypePB get dataType => ViewDataTypePB.Database; @override - ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Grid; + ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Grid; } class GridPluginConfig implements PluginConfig { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart index bbe22ffe8f..43a150d082 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart @@ -1,4 +1,5 @@ import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -56,7 +57,6 @@ class CellContainer extends StatelessWidget { child: Container( constraints: BoxConstraints(maxWidth: width, minHeight: 46), decoration: _makeBoxDecoration(context, isFocus), - padding: GridSize.cellContentInsets, child: container, ), ); @@ -92,8 +92,11 @@ class _GridCellEnterRegion extends StatelessWidget { builder: (context, onEnter, _) { List children = [child]; if (onEnter) { - children.add(CellAccessoryContainer(accessories: accessories) - .positioned(right: 0)); + children.add( + CellAccessoryContainer(accessories: accessories).positioned( + right: GridSize.cellContentInsets.right, + ), + ); } return MouseRegion( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart index adffa3ef16..e9bd8c79a4 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart @@ -4,6 +4,7 @@ import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../layout/sizes.dart'; import 'cell_builder.dart'; class GridCheckboxCell extends GridCellWidget { @@ -40,13 +41,16 @@ class _CheckboxCellState extends GridCellState { : svgWidget('editor/editor_uncheck'); return Align( alignment: Alignment.centerLeft, - child: FlowyIconButton( - onPressed: () => context - .read() - .add(const CheckboxCellEvent.select()), - iconPadding: EdgeInsets.zero, - icon: icon, - width: 20, + child: Padding( + padding: GridSize.cellContentInsets, + child: FlowyIconButton( + onPressed: () => context + .read() + .add(const CheckboxCellEvent.select()), + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + ), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart index f90b8d06f1..3213adff81 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart @@ -6,6 +6,7 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import '../../../layout/sizes.dart'; import '../cell_builder.dart'; import 'date_editor.dart'; @@ -75,7 +76,10 @@ class _DateCellState extends GridCellState { onTap: () => _popover.show(), child: Align( alignment: alignment, - child: FlowyText.medium(state.dateStr, fontSize: 12), + child: Padding( + padding: GridSize.cellContentInsets, + child: FlowyText.medium(state.dateStr, fontSize: 12), + ), ), ), ), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart index cd5151d750..83ba4270da 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart @@ -4,6 +4,7 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../layout/sizes.dart'; import 'cell_builder.dart'; class GridNumberCell extends GridCellWidget { @@ -45,18 +46,21 @@ class _NumberCellState extends GridFocusNodeCellState { _controller.text = contentFromState(state), ), ], - child: TextField( - controller: _controller, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: 1, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - textInputAction: TextInputAction.done, - decoration: const InputDecoration( - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - isDense: true, + child: Padding( + padding: GridSize.cellContentInsets, + child: TextField( + controller: _controller, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: 1, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + textInputAction: TextInputAction.done, + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + isDense: true, + ), ), ), ), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index 5ffea7dbb8..4d4b3ebb29 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -11,6 +11,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../layout/sizes.dart'; import '../cell_builder.dart'; import 'extension.dart'; import 'select_option_editor.dart'; @@ -170,7 +171,10 @@ class _SelectOptionWrapState extends State { alignment: AlignmentDirectional.center, fit: StackFit.expand, children: [ - _wrapPopover(child), + Padding( + padding: GridSize.cellContentInsets, + child: _wrapPopover(child), + ), InkWell(onTap: () => _popover.show()), ], ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart index 7586709f2c..9fd16c8f5d 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; @@ -170,10 +171,21 @@ class _SelectOptionTextFieldState extends State { .toList(); return Padding( padding: const EdgeInsets.all(8.0), - child: SingleChildScrollView( - controller: sc, - scrollDirection: Axis.horizontal, - child: Wrap(spacing: 4, children: children), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.trackpad, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, + }, + ), + child: SingleChildScrollView( + controller: sc, + scrollDirection: Axis.horizontal, + child: Wrap(spacing: 4, children: children), + ), ), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart index 0052ad5e70..cf418c6e93 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; +import '../../layout/sizes.dart'; import 'cell_builder.dart'; class GridTextCellStyle extends GridCellStyle { @@ -56,18 +57,21 @@ class _GridTextCellState extends GridFocusNodeCellState { _controller.text = state.content; } }, - child: TextField( - controller: _controller, - focusNode: focusNode, - onChanged: (value) => focusChanged(), - onEditingComplete: () => focusNode.unfocus(), - maxLines: null, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - decoration: InputDecoration( - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - hintText: widget.cellStyle?.placeholder, - isDense: true, + child: Padding( + padding: GridSize.cellContentInsets, + child: TextField( + controller: _controller, + focusNode: focusNode, + onChanged: (value) => focusChanged(), + onEditingComplete: () => focusNode.unfocus(), + maxLines: null, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + hintText: widget.cellStyle?.placeholder, + isDense: true, + ), ), ), ), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart index 04d10f9aa2..fbf626802e 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart @@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../../../layout/sizes.dart'; import '../cell_accessory.dart'; import '../cell_builder.dart'; import 'cell_editor.dart'; @@ -115,14 +116,17 @@ class _GridURLCellState extends GridCellState { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - final richText = RichText( - textAlign: TextAlign.left, - text: TextSpan( - text: state.content, - style: TextStyle( - color: theme.main2, - fontSize: 14, - decoration: TextDecoration.underline, + final richText = Padding( + padding: GridSize.cellContentInsets, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, + ), ), ), ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart index 6e55a345a2..36bae16b47 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; +import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:dartz/dartz.dart' show none; import 'package:easy_localization/easy_localization.dart'; @@ -58,19 +59,22 @@ class _FieldEditorState extends State { isGroupField: widget.isGroupField, loader: widget.typeOptionLoader, )..add(const FieldEditorEvent.initial()), - child: ListView( - shrinkWrap: true, - children: [ - FlowyText.medium( - LocaleKeys.grid_field_editProperty.tr(), - fontSize: 12, - ), - const VSpace(10), - _FieldNameTextField(popoverMutex: popoverMutex), - const VSpace(10), - ..._addDeleteFieldButton(), - _FieldTypeOptionCell(popoverMutex: popoverMutex), - ], + child: Padding( + padding: GridSize.typeOptionContentInsets, + child: ListView( + shrinkWrap: true, + children: [ + FlowyText.medium( + LocaleKeys.grid_field_editProperty.tr(), + fontSize: 12, + ), + const VSpace(10), + _FieldNameTextField(popoverMutex: popoverMutex), + const VSpace(10), + ..._addDeleteFieldButton(), + _FieldTypeOptionCell(popoverMutex: popoverMutex), + ], + ), ), ); } diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 7a53237dac..bd33115cde 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -118,11 +118,7 @@ void _resolveFolderDeps(GetIt getIt) { // AppPB getIt.registerFactoryParam( - (app, _) => AppBloc( - app: app, - appService: AppService(), - appListener: AppListener(appId: app.id), - ), + (app, _) => AppBloc(app: app), ); // trash diff --git a/frontend/app_flowy/lib/startup/plugin/plugin.dart b/frontend/app_flowy/lib/startup/plugin/plugin.dart index a8f58562fb..8e7f88768f 100644 --- a/frontend/app_flowy/lib/startup/plugin/plugin.dart +++ b/frontend/app_flowy/lib/startup/plugin/plugin.dart @@ -51,7 +51,7 @@ abstract class PluginBuilder { ViewDataTypePB get dataType => ViewDataTypePB.Text; - ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Document; + ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Document; } abstract class PluginConfig { diff --git a/frontend/app_flowy/lib/startup/tasks/rust_sdk.dart b/frontend/app_flowy/lib/startup/tasks/rust_sdk.dart index cdac0fa9e3..7e12c19f44 100644 --- a/frontend/app_flowy/lib/startup/tasks/rust_sdk.dart +++ b/frontend/app_flowy/lib/startup/tasks/rust_sdk.dart @@ -16,12 +16,12 @@ class InitRustSDKTask extends LaunchTask { } Future appFlowyDocumentDirectory() async { - Directory documentsDir = await getApplicationDocumentsDirectory(); - switch (integrationEnv()) { case IntegrationMode.develop: + Directory documentsDir = await getApplicationDocumentsDirectory(); return Directory('${documentsDir.path}/flowy_dev').create(); case IntegrationMode.release: + Directory documentsDir = await getApplicationDocumentsDirectory(); return Directory('${documentsDir.path}/flowy').create(); case IntegrationMode.test: return Directory("${Directory.current.path}/.sandbox"); diff --git a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart index 61db9a0cdf..fa2a9555bc 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart @@ -22,19 +22,24 @@ class AppBloc extends Bloc { final AppService appService; final AppListener appListener; - AppBloc( - {required this.app, required this.appService, required this.appListener}) - : super(AppState.initial(app)) { + AppBloc({required this.app}) + : appService = AppService(), + appListener = AppListener(appId: app.id), + super(AppState.initial(app)) { on((event, emit) async { await event.map(initial: (e) async { _startListening(); await _loadViews(emit); }, createView: (CreateView value) async { await _createView(value, emit); + }, loadViews: (_) async { + await _loadViews(emit); }, didReceiveViewUpdated: (e) async { await _didReceiveViewUpdated(e.views, emit); }, delete: (e) async { - await _deleteView(emit); + await _deleteApp(emit); + }, deleteView: (deletedView) async { + await _deleteView(emit, deletedView.viewId); }, rename: (e) async { await _renameView(e, emit); }, appDidUpdate: (e) async { @@ -71,7 +76,8 @@ class AppBloc extends Bloc { ); } - Future _deleteView(Emitter emit) async { +// Delete the current app + Future _deleteApp(Emitter emit) async { final result = await appService.delete(appId: app.id); result.fold( (unit) => emit(state.copyWith(successOrFailure: left(unit))), @@ -79,16 +85,24 @@ class AppBloc extends Bloc { ); } + Future _deleteView(Emitter emit, String viewId) async { + final result = await appService.deleteView(viewId: viewId); + result.fold( + (unit) => emit(state.copyWith(successOrFailure: left(unit))), + (error) => emit(state.copyWith(successOrFailure: right(error))), + ); + } + Future _createView(CreateView value, Emitter emit) async { - final viewOrFailed = await appService.createView( + final result = await appService.createView( appId: app.id, name: value.name, - desc: value.desc, - dataType: value.dataType, - pluginType: value.pluginType, - layout: value.layout, + desc: value.desc ?? "", + dataType: value.pluginBuilder.dataType, + pluginType: value.pluginBuilder.pluginType, + layoutType: value.pluginBuilder.layoutType!, ); - viewOrFailed.fold( + result.fold( (view) => emit(state.copyWith( latestCreatedView: view, successOrFailure: left(unit), @@ -107,7 +121,9 @@ class AppBloc extends Bloc { } Future _didReceiveViewUpdated( - List views, Emitter emit) async { + List views, + Emitter emit, + ) async { final latestCreatedView = state.latestCreatedView; AppState newState = state.copyWith(views: views); if (latestCreatedView != null) { @@ -138,12 +154,12 @@ class AppEvent with _$AppEvent { const factory AppEvent.initial() = Initial; const factory AppEvent.createView( String name, - String desc, - ViewDataTypePB dataType, - ViewLayoutTypePB layout, - PluginType pluginType, - ) = CreateView; - const factory AppEvent.delete() = Delete; + PluginBuilder pluginBuilder, { + String? desc, + }) = CreateView; + const factory AppEvent.loadViews() = LoadApp; + const factory AppEvent.delete() = DeleteApp; + const factory AppEvent.deleteView(String viewId) = DeleteView; const factory AppEvent.rename(String newName) = Rename; const factory AppEvent.didReceiveViewUpdated(List views) = ReceiveViews; @@ -161,7 +177,7 @@ class AppState with _$AppState { factory AppState.initial(AppPB app) => AppState( app: app, - views: [], + views: app.belongings.items, successOrFailure: left(unit), ); } diff --git a/frontend/app_flowy/lib/workspace/application/app/app_service.dart b/frontend/app_flowy/lib/workspace/application/app/app_service.dart index 9f64a68326..ab35c3338d 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_service.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_service.dart @@ -18,17 +18,17 @@ class AppService { Future> createView({ required String appId, required String name, - required String desc, + String? desc, required ViewDataTypePB dataType, required PluginType pluginType, - required ViewLayoutTypePB layout, + required ViewLayoutTypePB layoutType, }) { var payload = CreateViewPayloadPB.create() ..belongToId = appId ..name = name - ..desc = desc + ..desc = desc ?? "" ..dataType = dataType - ..layout = layout; + ..layout = layoutType; return FolderEventCreateView(payload).send(); } @@ -49,6 +49,11 @@ class AppService { return FolderEventDeleteApp(request).send(); } + Future> deleteView({required String viewId}) { + final request = RepeatedViewIdPB.create()..items.add(viewId); + return FolderEventDeleteView(request).send(); + } + Future> updateApp( {required String appId, String? name}) { UpdateAppPayloadPB payload = UpdateAppPayloadPB.create()..appId = appId; diff --git a/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart b/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart index bf3cfee6e3..e343b6f9fe 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart @@ -31,12 +31,14 @@ class ViewBloc extends Bloc { }, viewDidUpdate: (e) { e.result.fold( - (view) => emit(state.copyWith(view: view, successOrFailure: left(unit))), + (view) => + emit(state.copyWith(view: view, successOrFailure: left(unit))), (error) => emit(state.copyWith(successOrFailure: right(error))), ); }, rename: (e) async { - final result = await service.updateView(viewId: view.id, name: e.newName); + final result = + await service.updateView(viewId: view.id, name: e.newName); emit( result.fold( (l) => state.copyWith(successOrFailure: left(unit)), @@ -46,7 +48,6 @@ class ViewBloc extends Bloc { }, delete: (e) async { final result = await service.delete(viewId: view.id); - await service.updateView(viewId: view.id); emit( result.fold( (l) => state.copyWith(successOrFailure: left(unit)), @@ -81,7 +82,8 @@ class ViewEvent with _$ViewEvent { const factory ViewEvent.rename(String newName) = Rename; const factory ViewEvent.delete() = Delete; const factory ViewEvent.duplicate() = Duplicate; - const factory ViewEvent.viewDidUpdate(Either result) = ViewDidUpdate; + const factory ViewEvent.viewDidUpdate(Either result) = + ViewDidUpdate; } @freezed diff --git a/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart b/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart index 295b7c3af7..c6b7d4cba3 100644 --- a/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart @@ -17,11 +17,11 @@ class WorkspaceService { required this.workspaceId, }); Future> createApp( - {required String name, required String desc}) { + {required String name, String? desc}) { final payload = CreateAppPayloadPB.create() ..name = name ..workspaceId = workspaceId - ..desc = desc; + ..desc = desc ?? ""; return FolderEventCreateApp(payload).send(); } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart index afaecdbd45..d887b3417e 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart @@ -104,13 +104,12 @@ class MenuAppHeader extends StatelessWidget { message: LocaleKeys.menuAppHeader_addPageTooltip.tr(), child: AddButton( onSelected: (pluginBuilder) { - context.read().add(AppEvent.createView( - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - "", - pluginBuilder.dataType, - pluginBuilder.subDataType!, - pluginBuilder.pluginType, - )); + context.read().add( + AppEvent.createView( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + pluginBuilder, + ), + ); }, ).padding(right: MenuAppSizes.headerPadding), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart index c7ee59d03e..b4a2e3532c 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart @@ -50,7 +50,6 @@ class _MenuAppState extends State { }, ), BlocListener( - listenWhen: (p, c) => p.views != c.views, listener: (context, state) => viewDataContext.views = state.views, ), ], diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart index 259299b964..1b37e5fa11 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart @@ -188,9 +188,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) { return KeyEventResult.handled; }; -/// To create a link, enclose the link text in brackets (e.g., [link text]). -/// Then, immediately follow it with the URL in parentheses (e.g., (https://example.com)). -ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) { +ShortcutEventHandler markdownLinkOrImageHandler = (editorState, event) { final selectionService = editorState.service.selectionService; final selection = selectionService.currentSelection.value; final textNodes = selectionService.currentSelectedNodes.whereType(); @@ -198,48 +196,72 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) { return KeyEventResult.ignored; } - // find all of the indexs for important characters + // Find all of the indexes of the relevant characters final textNode = textNodes.first; final text = textNode.toPlainText(); + final firstExclamation = text.indexOf('!'); final firstOpeningBracket = text.indexOf('['); final firstClosingBracket = text.indexOf(']'); - // use regex to validate the format of the link - // note: this enforces that the link has http or https - final regexp = RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d./?=#]+)$'); - final match = regexp.firstMatch(text); - if (match == null) { + // Use RegEx to determine whether it's an image or a link + // Difference between image and link syntax is that image + // has an exclamation point at the beginning. + // Note: The RegEx enforces that the URL has http or https + final imgRegEx = + RegExp(r'\!\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d-./?=#%&]+)$'); + final lnkRegEx = + RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d-./?=#%&]+)$'); + + if (imgRegEx.firstMatch(text) != null) { + // Extract the alt text and the URL of the image + final match = lnkRegEx.firstMatch(text); + final imgUrl = match?.group(2); + + // Delete the text and replace it with the image pointed to by the URL + final transaction = editorState.transaction + ..deleteText(textNode, firstExclamation, text.length) + ..insertNode( + textNode.path, + Node.fromJson({ + 'type': 'image', + 'attributes': { + 'image_src': imgUrl, + 'align': 'center', + } + })); + editorState.apply(transaction); + } else if (lnkRegEx.firstMatch(text) != null) { + // Extract the text and the URL of the link + final match = lnkRegEx.firstMatch(text); + final linkText = match?.group(1); + final linkUrl = match?.group(2); + + // Delete the initial opening bracket, + // update the href attribute of the text surrounded by [ ] to the url, + // delete everything after the text, + // and update the cursor position. + final transaction = editorState.transaction + ..deleteText(textNode, firstOpeningBracket, 1) + ..formatText( + textNode, + firstOpeningBracket, + firstClosingBracket - firstOpeningBracket - 1, + { + BuiltInAttributeKey.href: linkUrl, + }, + ) + ..deleteText(textNode, firstClosingBracket - 1, + selection.end.offset - firstClosingBracket) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: firstOpeningBracket + linkText!.length, + ), + ); + editorState.apply(transaction); + } else { return KeyEventResult.ignored; } - - // extract the text and the url of the link - final linkText = match.group(1); - final linkUrl = match.group(2); - - // Delete the initial opening bracket, - // update the href attribute of the text surrounded by [ ] to the url, - // delete everything after the text, - // and update the cursor position. - final transaction = editorState.transaction - ..deleteText(textNode, firstOpeningBracket, 1) - ..formatText( - textNode, - firstOpeningBracket, - firstClosingBracket - firstOpeningBracket - 1, - { - BuiltInAttributeKey.href: linkUrl, - }, - ) - ..deleteText(textNode, firstClosingBracket - 1, - selection.end.offset - firstClosingBracket) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path, - offset: firstOpeningBracket + linkText!.length, - ), - ); - editorState.apply(transaction); - return KeyEventResult.handled; }; @@ -369,6 +391,5 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { ), ); editorState.apply(transaction); - return KeyEventResult.handled; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 0b6a006cc3..3f831333ca 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -16,7 +16,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespa import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; import 'package:flutter/foundation.dart'; -// List builtInShortcutEvents = [ ShortcutEvent( key: 'Move cursor up', @@ -276,9 +275,9 @@ List builtInShortcutEvents = [ handler: doubleTildeToStrikethrough, ), ShortcutEvent( - key: 'Markdown link to link', + key: 'Markdown link or image', command: 'shift+parenthesis right', - handler: markdownLinkToLinkHandler, + handler: markdownLinkOrImageHandler, ), // https://github.com/flutter/flutter/issues/104944 // Workaround: Using space editing on the web platform often results in errors, diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart b/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart index 5ced5b2e83..6d9d5a2416 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart @@ -7,10 +7,10 @@ import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart' as Foundation; // ignore_for_file: unused_import, camel_case_types, non_constant_identifier_names -final DynamicLibrary _dl = _open(); +final DynamicLibrary _dart_ffi_lib = _open(); /// Reference to the Dynamic Library, it should be only used for low-level access -final DynamicLibrary dl = _dl; +final DynamicLibrary dl = _dart_ffi_lib; DynamicLibrary _open() { if (Platform.environment.containsKey('FLUTTER_TEST')) { final prefix = "${Directory.current.path}/.sandbox"; @@ -18,7 +18,8 @@ DynamicLibrary _open() { return DynamicLibrary.open('${prefix}/libdart_ffi.so'); if (Platform.isAndroid) return DynamicLibrary.open('${prefix}/libdart_ffi.so'); - if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a'); + if (Platform.isMacOS) + return DynamicLibrary.open('${prefix}/libdart_ffi.dylib'); if (Platform.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a'); if (Platform.isWindows) return DynamicLibrary.open('${prefix}/dart_ffi.dll'); @@ -42,8 +43,8 @@ void async_event( _invoke_async(port, input, len); } -final _invoke_async_Dart _invoke_async = - _dl.lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event'); +final _invoke_async_Dart _invoke_async = _dart_ffi_lib + .lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event'); typedef _invoke_async_C = Void Function( Int64 port, Pointer input, @@ -63,8 +64,8 @@ Pointer sync_event( return _invoke_sync(input, len); } -final _invoke_sync_Dart _invoke_sync = - _dl.lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event'); +final _invoke_sync_Dart _invoke_sync = _dart_ffi_lib + .lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event'); typedef _invoke_sync_C = Pointer Function( Pointer input, Uint64 len, @@ -82,7 +83,7 @@ int init_sdk( } final _init_sdk_Dart _init_sdk = - _dl.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk'); + _dart_ffi_lib.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk'); typedef _init_sdk_C = Int64 Function( Pointer path, ); @@ -96,7 +97,7 @@ int set_stream_port(int port) { } final _set_stream_port_Dart _set_stream_port = - _dl.lookupFunction<_set_stream_port_C, _set_stream_port_Dart>( + _dart_ffi_lib.lookupFunction<_set_stream_port_C, _set_stream_port_Dart>( 'set_stream_port'); typedef _set_stream_port_C = Int32 Function( @@ -111,7 +112,7 @@ void link_me_please() { _link_me_please(); } -final _link_me_please_Dart _link_me_please = _dl +final _link_me_please_Dart _link_me_please = _dart_ffi_lib .lookupFunction<_link_me_please_C, _link_me_please_Dart>('link_me_please'); typedef _link_me_please_C = Void Function(); typedef _link_me_please_Dart = void Function(); @@ -123,7 +124,7 @@ void store_dart_post_cobject( _store_dart_post_cobject(ptr); } -final _store_dart_post_cobject_Dart _store_dart_post_cobject = _dl +final _store_dart_post_cobject_Dart _store_dart_post_cobject = _dart_ffi_lib .lookupFunction<_store_dart_post_cobject_C, _store_dart_post_cobject_Dart>( 'store_dart_post_cobject'); typedef _store_dart_post_cobject_C = Void Function( diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index b10cbf3629..f280d3b4ca 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -49,7 +49,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.1" + version: "3.1.11" args: dependency: transitive description: @@ -245,7 +245,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.3.2" + version: "1.2.0" cross_file: dependency: transitive description: @@ -259,7 +259,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.1" csslib: dependency: transitive description: @@ -454,6 +454,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.1" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_inappwebview: dependency: transitive description: @@ -555,6 +560,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" get_it: dependency: "direct main" description: @@ -660,6 +670,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.5.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -1246,6 +1261,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.1+2" + sync_http: + dependency: transitive + description: + name: sync_http + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" table_calendar: dependency: "direct main" description: @@ -1322,7 +1344,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" universal_platform: dependency: transitive description: @@ -1441,7 +1463,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "8.3.0" + version: "8.2.2" watcher: dependency: transitive description: @@ -1456,6 +1478,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.0" + webdriver: + dependency: transitive + description: + name: webdriver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" webkit_inspection_protocol: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index d689ed688e..302d8b920e 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -97,6 +97,8 @@ dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter build_runner: ^2.2.0 freezed: ^2.1.0+1 bloc_test: ^9.0.2 diff --git a/frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart new file mode 100644 index 0000000000..b2bfa6996f --- /dev/null +++ b/frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart @@ -0,0 +1,44 @@ +import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + group('GridBloc', () { + blocTest( + "Create row", + build: () => + GridBloc(view: gridTest.gridView)..add(const GridEvent.initial()), + act: (bloc) => bloc.add(const GridEvent.createRow()), + wait: const Duration(milliseconds: 300), + verify: (bloc) { + assert(bloc.state.rowInfos.length == 4); + }, + ); + }); + + group('GridBloc', () { + late GridBloc gridBloc; + setUpAll(() async { + gridBloc = GridBloc(view: gridTest.gridView) + ..add(const GridEvent.initial()); + await gridResponseFuture(); + }); + + // The initial number of rows is three + test('', () async { + assert(gridBloc.state.rowInfos.length == 3); + }); + + test('delete row', () async { + gridBloc.add(GridEvent.deleteRow(gridBloc.state.rowInfos.last)); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 2); + }); + }); +} diff --git a/frontend/app_flowy/test/bloc_test/grid_test/select_option_bloc_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/select_option_bloc_test.dart new file mode 100644 index 0000000000..bea51b4960 --- /dev/null +++ b/frontend/app_flowy/test/bloc_test/grid_test/select_option_bloc_test.dart @@ -0,0 +1,36 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'util.dart'; + +void main() { + late AppFlowyGridSelectOptionCellTest cellTest; + setUpAll(() async { + cellTest = await AppFlowyGridSelectOptionCellTest.ensureInitialized(); + }); + + group('SingleSelectOptionBloc', () { + late GridSelectOptionCellController cellController; + setUp(() async { + cellController = + await cellTest.makeCellController(FieldType.SingleSelect); + }); + + blocTest( + "create option", + build: () { + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); + return bloc; + }, + act: (bloc) => bloc.add(const SelectOptionEditorEvent.newOption("A")), + wait: gridResponseDuration(), + verify: (bloc) { + assert(bloc.state.options.length == 1); + assert(bloc.state.options[0].name == "A"); + }, + ); + }); +} diff --git a/frontend/app_flowy/test/bloc_test/grid_test/util.dart b/frontend/app_flowy/test/bloc_test/grid_test/util.dart new file mode 100644 index 0000000000..e81028dedc --- /dev/null +++ b/frontend/app_flowy/test/bloc_test/grid_test/util.dart @@ -0,0 +1,124 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; +import 'package:app_flowy/plugins/grid/grid.dart'; +import 'package:app_flowy/workspace/application/app/app_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; + +import '../../util.dart'; + +/// Create a empty Grid for test +class AppFlowyGridTest { + // ignore: unused_field + final AppFlowyUnitTest _inner; + late ViewPB gridView; + AppFlowyGridTest(AppFlowyUnitTest unitTest) : _inner = unitTest; + + static Future ensureInitialized() async { + final inner = await AppFlowyUnitTest.ensureInitialized(); + final test = AppFlowyGridTest(inner); + await test._createTestGrid(); + return test; + } + + Future _createTestGrid() async { + final app = await _inner.createTestApp(); + final builder = GridPluginBuilder(); + final result = await AppService().createView( + appId: app.id, + name: "Test Grid", + dataType: builder.dataType, + pluginType: builder.pluginType, + layoutType: builder.layoutType!, + ); + result.fold( + (view) => gridView = view, + (error) {}, + ); + } +} + +class AppFlowyGridSelectOptionCellTest { + final AppFlowyGridCellTest _cellTest; + + AppFlowyGridSelectOptionCellTest(AppFlowyGridCellTest cellTest) + : _cellTest = cellTest; + + static Future ensureInitialized() async { + final cellTest = await AppFlowyGridCellTest.ensureInitialized(); + final test = AppFlowyGridSelectOptionCellTest(cellTest); + return test; + } + + /// For the moment, just edit the first row of the grid. + Future makeCellController( + FieldType fieldType) async { + assert(fieldType == FieldType.SingleSelect || + fieldType == FieldType.MultiSelect); + + final fieldContexts = + _cellTest._dataController.fieldController.fieldContexts; + final field = + fieldContexts.firstWhere((element) => element.fieldType == fieldType); + final builder = await _cellTest.cellControllerBuilder(0, field.id); + final cellController = builder.build() as GridSelectOptionCellController; + return cellController; + } +} + +class AppFlowyGridCellTest { + // ignore: unused_field + final AppFlowyGridTest _gridTest; + final GridDataController _dataController; + AppFlowyGridCellTest(AppFlowyGridTest gridTest) + : _gridTest = gridTest, + _dataController = GridDataController(view: gridTest.gridView); + + static Future ensureInitialized() async { + final gridTest = await AppFlowyGridTest.ensureInitialized(); + final test = AppFlowyGridCellTest(gridTest); + await test._loadGridData(); + return test; + } + + Future _loadGridData() async { + final result = await _dataController.loadData(); + result.fold((l) => null, (r) => throw Exception(r)); + } + + Future cellControllerBuilder( + int rowIndex, String fieldId) async { + final RowInfo rowInfo = _dataController.rowInfos[rowIndex]; + final blockCache = _dataController.blocks[rowInfo.rowPB.blockId]; + final rowCache = blockCache?.rowCache; + + final rowDataController = GridRowDataController( + rowInfo: rowInfo, + fieldController: _dataController.fieldController, + rowCache: rowCache!, + ); + + final rowBloc = RowBloc( + rowInfo: rowInfo, + dataController: rowDataController, + )..add(const RowEvent.initial()); + await gridResponseFuture(milliseconds: 300); + + return GridCellControllerBuilder( + cellId: rowBloc.state.gridCellMap[fieldId]!, + cellCache: rowCache.cellCache, + delegate: rowDataController, + ); + } +} + +Future gridResponseFuture({int milliseconds = 200}) { + return Future.delayed(gridResponseDuration(milliseconds: milliseconds)); +} + +Duration gridResponseDuration({int milliseconds = 200}) { + return Duration(milliseconds: milliseconds); +} diff --git a/frontend/app_flowy/test/bloc_test/menu_test/app_bloc_test.dart b/frontend/app_flowy/test/bloc_test/menu_test/app_bloc_test.dart new file mode 100644 index 0000000000..e7c0477636 --- /dev/null +++ b/frontend/app_flowy/test/bloc_test/menu_test/app_bloc_test.dart @@ -0,0 +1,104 @@ +import 'package:app_flowy/plugins/board/board.dart'; +import 'package:app_flowy/plugins/doc/document.dart'; +import 'package:app_flowy/plugins/grid/grid.dart'; +import 'package:app_flowy/workspace/application/app/app_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:bloc_test/bloc_test.dart'; +import '../../util.dart'; + +void main() { + late AppFlowyUnitTest test; + setUpAll(() async { + test = await AppFlowyUnitTest.ensureInitialized(); + }); + + group( + 'AppBloc', + () { + late AppPB app; + setUp(() async { + app = await test.createTestApp(); + }); + + blocTest( + "Create a document", + build: () => AppBloc(app: app)..add(const AppEvent.initial()), + act: (bloc) { + bloc.add( + AppEvent.createView("Test document", DocumentPluginBuilder())); + }, + wait: blocResponseDuration(), + verify: (bloc) { + assert(bloc.state.views.length == 1); + assert(bloc.state.views.last.name == "Test document"); + assert(bloc.state.views.last.layout == ViewLayoutTypePB.Document); + }, + ); + + blocTest( + "Create a grid", + build: () => AppBloc(app: app)..add(const AppEvent.initial()), + act: (bloc) { + bloc.add(AppEvent.createView("Test grid", GridPluginBuilder())); + }, + wait: blocResponseDuration(), + verify: (bloc) { + assert(bloc.state.views.length == 1); + assert(bloc.state.views.last.name == "Test grid"); + assert(bloc.state.views.last.layout == ViewLayoutTypePB.Grid); + }, + ); + + blocTest( + "Create a Kanban board", + build: () => AppBloc(app: app)..add(const AppEvent.initial()), + act: (bloc) { + bloc.add(AppEvent.createView("Test board", BoardPluginBuilder())); + }, + wait: const Duration(milliseconds: 100), + verify: (bloc) { + assert(bloc.state.views.length == 1); + assert(bloc.state.views.last.name == "Test board"); + assert(bloc.state.views.last.layout == ViewLayoutTypePB.Board); + }, + ); + }, + ); + + group('AppBloc', () { + late ViewPB view; + late AppPB app; + setUpAll(() async { + app = await test.createTestApp(); + }); + + blocTest( + "create a document", + build: () => AppBloc(app: app)..add(const AppEvent.initial()), + act: (bloc) { + bloc.add(AppEvent.createView("Test document", DocumentPluginBuilder())); + }, + wait: blocResponseDuration(), + verify: (bloc) { + assert(bloc.state.views.length == 1); + view = bloc.state.views.last; + }, + ); + blocTest( + "delete the document", + build: () => AppBloc(app: app)..add(const AppEvent.initial()), + act: (bloc) => bloc.add(AppEvent.deleteView(view.id)), + ); + blocTest( + "verify the document is exist", + build: () => AppBloc(app: app)..add(const AppEvent.initial()), + act: (bloc) => bloc.add(const AppEvent.loadViews()), + wait: blocResponseDuration(), + verify: (bloc) { + assert(bloc.state.views.isEmpty); + }, + ); + }); +} diff --git a/frontend/app_flowy/test/util.dart b/frontend/app_flowy/test/util.dart new file mode 100644 index 0000000000..b8367f90fe --- /dev/null +++ b/frontend/app_flowy/test/util.dart @@ -0,0 +1,109 @@ +import 'package:app_flowy/startup/startup.dart'; +import 'package:app_flowy/user/application/auth_service.dart'; +import 'package:app_flowy/user/application/user_service.dart'; +import 'package:app_flowy/workspace/application/workspace/workspace_service.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:app_flowy/main.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppFlowyIntegrateTest { + static Future ensureInitialized() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + main(); + return AppFlowyIntegrateTest(); + } +} + +class AppFlowyUnitTest { + late UserProfilePB userProfile; + late UserService userService; + late WorkspaceService workspaceService; + late List workspaces; + + static Future ensureInitialized() async { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + _pathProviderInitialized(); + + await EasyLocalization.ensureInitialized(); + await FlowyRunner.run(FlowyTestApp()); + + final test = AppFlowyUnitTest(); + await test._signIn(); + await test._loadWorkspace(); + + await test._initialServices(); + return test; + } + + Future _signIn() async { + final authService = getIt(); + const password = "AppFlowy123@"; + final uid = uuid(); + final userEmail = "$uid@appflowy.io"; + final result = await authService.signUp( + name: "TestUser", + password: password, + email: userEmail, + ); + return result.fold( + (user) { + userProfile = user; + userService = UserService(userId: userProfile.id); + }, + (error) {}, + ); + } + + WorkspacePB get currentWorkspace => workspaces[0]; + + Future _loadWorkspace() async { + final result = await userService.getWorkspaces(); + result.fold( + (value) => workspaces = value, + (error) { + throw Exception(error); + }, + ); + } + + Future _initialServices() async { + workspaceService = WorkspaceService(workspaceId: currentWorkspace.id); + } + + Future createTestApp() async { + final result = await workspaceService.createApp(name: "Test App"); + return result.fold( + (app) => app, + (error) => throw Exception(error), + ); + } +} + +void _pathProviderInitialized() { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/path_provider'); + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return "."; + }); +} + +class FlowyTestApp implements EntryPoint { + @override + Widget create() { + return Container(); + } +} + +Duration blocResponseDuration({int millseconds = 100}) { + return Duration(milliseconds: millseconds); +} diff --git a/frontend/app_flowy/test/util/test_env.dart b/frontend/app_flowy/test/util/test_env.dart deleted file mode 100644 index 1743e4b920..0000000000 --- a/frontend/app_flowy/test/util/test_env.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:app_flowy/startup/startup.dart'; -import 'package:app_flowy/user/application/auth_service.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class FlowyTest { - static Future setup() async { - TestWidgetsFlutterBinding.ensureInitialized(); - // await EasyLocalization.ensureInitialized(); - - await FlowyRunner.run(FlowyTestApp()); - return FlowyTest(); - } - - Future signIn() async { - final authService = getIt(); - const password = "AppFlowy123@"; - final uid = uuid(); - final userEmail = "$uid@appflowy.io"; - final result = await authService.signUp( - name: "FlowyTestUser", - password: password, - email: userEmail, - ); - return result.fold( - (user) => user, - (error) { - throw StateError("$error"); - }, - ); - } -} - -class FlowyTestApp implements EntryPoint { - @override - Widget create() { - return Container(); - } -} diff --git a/frontend/app_flowy/test/workspace_bloc_test.dart b/frontend/app_flowy/test/workspace_bloc_test.dart deleted file mode 100644 index d0b898cc42..0000000000 --- a/frontend/app_flowy/test/workspace_bloc_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:app_flowy/startup/startup.dart'; -import 'package:app_flowy/workspace/application/workspace/welcome_bloc.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:bloc_test/bloc_test.dart'; - -import 'util/test_env.dart'; - -void main() { - UserProfilePB? userInfo; - setUpAll(() async { - final flowyTest = await FlowyTest.setup(); - userInfo = await flowyTest.signIn(); - }); - - group('WelcomeBloc', () { - blocTest( - "welcome screen init", - build: () => getIt(param1: userInfo), - act: (bloc) { - bloc.add(const WelcomeEvent.initial()); - }, - wait: const Duration(seconds: 3), - verify: (bloc) { - assert(bloc.state.isLoading == false); - }, - ); - }); -} diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 24811ca889..fcc6c7c9d2 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -93,10 +93,9 @@ impl GridManager { Ok(()) } - #[tracing::instrument(level = "debug", skip_all, fields(grid_id), err)] + #[tracing::instrument(level = "debug", skip_all, err)] pub async fn open_grid>(&self, grid_id: T) -> FlowyResult> { let grid_id = grid_id.as_ref(); - tracing::Span::current().record("grid_id", &grid_id); let _ = self.migration.run_v1_migration(grid_id).await; self.get_or_create_grid_editor(grid_id).await } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs index c731d27b02..9932566605 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs @@ -1,6 +1,7 @@ mod multi_select_type_option; mod select_type_option; mod single_select_type_option; +mod type_option_transform; pub use multi_select_type_option::*; pub use select_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 5ddf4d00f2..a7a2e52732 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -1,6 +1,7 @@ use crate::entities::FieldType; use crate::impl_type_option; use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformer; use crate::services::field::type_options::util::get_cell_data; use crate::services::field::{ BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction, @@ -110,7 +111,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder { } fn transform(&mut self, field_type: &FieldType, type_option_data: String) { - self.0.transform_type_option(field_type, type_option_data); + SelectOptionTypeOptionTransformer::transform_type_option(&mut self.0, field_type, type_option_data) } } #[cfg(test)] diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_type_option.rs index a99b1e48b0..06244239b6 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_type_option.rs @@ -2,7 +2,8 @@ use crate::entities::{CellChangesetPB, FieldType, GridCellIdPB, GridCellIdParams use crate::services::cell::{ CellBytes, CellBytesParser, CellData, CellDataIsEmpty, CellDisplayable, FromCellChangeset, FromCellString, }; -use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB, CHECK, UNCHECK}; +use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformer; +use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB}; use bytes::Bytes; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::{internal_error, ErrorCode, FlowyResult}; @@ -119,25 +120,6 @@ pub trait SelectTypeOptionSharedAction: TypeOptionDataSerializer + Send + Sync { } } - fn transform_type_option(&mut self, field_type: &FieldType, _type_option_data: String) { - match field_type { - FieldType::Checkbox => { - //add Yes and No options if it does not exist. - if !self.options().iter().any(|option| option.name == CHECK) { - let check_option = SelectOptionPB::with_color(CHECK, SelectOptionColorPB::Green); - self.mut_options().push(check_option); - } - - if !self.options().iter().any(|option| option.name == UNCHECK) { - let uncheck_option = SelectOptionPB::with_color(UNCHECK, SelectOptionColorPB::Yellow); - self.mut_options().push(uncheck_option); - } - } - FieldType::MultiSelect => {} - _ => {} - } - } - fn transform_cell_data( &self, cell_data: CellData, @@ -150,6 +132,21 @@ pub trait SelectTypeOptionSharedAction: TypeOptionDataSerializer + Send + Sync { } FieldType::Checkbox => { // transform the cell data to the option id + let mut transformed_ids = Vec::new(); + let options = self.options(); + cell_data.0.iter().for_each(|ids| { + ids.0.iter().for_each(|name| { + let id = options + .iter() + .find(|option| option.name == name.clone()) + .unwrap() + .id + .clone(); + transformed_ids.push(id); + }) + }); + + return CellBytes::from(self.get_selected_options(CellData(Some(SelectOptionIds(transformed_ids))))); } _ => { return Ok(CellBytes::default()); @@ -174,7 +171,12 @@ where decoded_field_type: &FieldType, field_rev: &FieldRevision, ) -> FlowyResult { - self.transform_cell_data(cell_data, decoded_field_type, field_rev) + SelectOptionTypeOptionTransformer::transform_type_option_cell_data( + self, + cell_data, + decoded_field_type, + field_rev, + ) } fn displayed_cell_string( diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs index 2f1dfa409c..eb164a42ef 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -1,6 +1,7 @@ use crate::entities::FieldType; use crate::impl_type_option; use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformer; use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use crate::services::field::{ SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction, @@ -96,7 +97,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder { } fn transform(&mut self, field_type: &FieldType, type_option_data: String) { - self.0.transform_type_option(field_type, type_option_data); + SelectOptionTypeOptionTransformer::transform_type_option(&mut self.0, field_type, type_option_data) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/type_option_transform.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/type_option_transform.rs new file mode 100644 index 0000000000..b829fa148b --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/type_option_transform.rs @@ -0,0 +1,63 @@ +use crate::entities::FieldType; +use crate::services::cell::{CellBytes, CellData}; +use crate::services::field::{ + SelectOptionColorPB, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction, CHECK, UNCHECK, +}; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::FieldRevision; + +/// Handles how to transform the cell data when switching between different field types +pub struct SelectOptionTypeOptionTransformer(); +impl SelectOptionTypeOptionTransformer { + pub fn transform_type_option(shared: &mut T, field_type: &FieldType, _type_option_data: String) + where + T: SelectTypeOptionSharedAction, + { + match field_type { + FieldType::Checkbox => { + //add Yes and No options if it does not exist. + if !shared.options().iter().any(|option| option.name == CHECK) { + let check_option = SelectOptionPB::with_color(CHECK, SelectOptionColorPB::Green); + shared.mut_options().push(check_option); + } + + if !shared.options().iter().any(|option| option.name == UNCHECK) { + let uncheck_option = SelectOptionPB::with_color(UNCHECK, SelectOptionColorPB::Yellow); + shared.mut_options().push(uncheck_option); + } + } + FieldType::MultiSelect => {} + _ => {} + } + } + + pub fn transform_type_option_cell_data( + shared: &T, + cell_data: CellData, + decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult + where + T: SelectTypeOptionSharedAction, + { + match decoded_field_type { + FieldType::SingleSelect | FieldType::MultiSelect => { + // + CellBytes::from(shared.get_selected_options(cell_data)) + } + FieldType::Checkbox => { + // transform the cell data to the option id + let mut transformed_ids = Vec::new(); + let options = shared.options(); + cell_data.try_into_inner()?.iter().for_each(|name| { + if let Some(option) = options.iter().find(|option| &option.name == name) { + transformed_ids.push(option.id.clone()); + } + }); + let transformed_cell_data = CellData::from(SelectOptionIds::from(transformed_ids)); + CellBytes::from(shared.get_selected_options(transformed_cell_data)) + } + _ => Ok(CellBytes::default()), + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index 7614f5af37..79bd48a2b0 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -31,11 +31,13 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder { } } +/// For the moment, the `RichTextTypeOptionPB` is empty. The `data` property is not +/// used yet. #[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)] pub struct RichTextTypeOptionPB { #[pb(index = 1)] #[serde(default)] - data: String, //It's not used yet + data: String, } impl_type_option!(RichTextTypeOptionPB, FieldType::RichText); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index a931dddf66..70e791740b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -100,15 +100,15 @@ impl GridRevisionEditor { /// /// * `grid_id`: the id of the grid /// * `field_id`: the id of the field - /// * `type_option_data`: the updated type-option data. - /// + /// * `type_option_data`: the updated type-option data. The `type-option` data might be empty + /// if there is no type-option config for that field. For example, the `RichTextTypeOptionPB`. + /// pub async fn update_field_type_option( &self, grid_id: &str, field_id: &str, type_option_data: Vec, ) -> FlowyResult<()> { - debug_assert!(!type_option_data.is_empty()); if type_option_data.is_empty() { return Ok(()); } diff --git a/frontend/scripts/makefile/desktop.toml b/frontend/scripts/makefile/desktop.toml index 09b1680f3f..b97770378b 100644 --- a/frontend/scripts/makefile/desktop.toml +++ b/frontend/scripts/makefile/desktop.toml @@ -21,17 +21,18 @@ run_task = { name = ["setup-crate-type","sdk-build-android", "restore-crate-type [tasks.flowy-sdk-dev-macos] category = "Build" dependencies = ["env_check"] -run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type", "copy-to-sys-tmpdir"] } +run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type"] } [tasks.flowy-sdk-dev-windows] category = "Build" dependencies = ["env_check"] -run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type", "copy-to-sys-tmpdir"] } +run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type"] } [tasks.flowy-sdk-dev-linux] category = "Build" dependencies = ["env_check"] -run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type", "copy-to-sys-tmpdir"] } +run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type"] } + # [tasks.sdk-build] @@ -114,7 +115,7 @@ script = [ """ echo "🚀 🚀 🚀 Flowy-SDK(macOS) build success" dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/packages/flowy_sdk/${TARGET_OS} - lib = set lib${LIB_NAME}.${SDK_EXT} + lib = set lib${LIB_NAME}.${LIB_EXT} cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \ ${dart_ffi_dir}/${lib} @@ -131,7 +132,7 @@ script = [ """ echo "🚀 🚀 🚀 Flowy-SDK(windows) build success" dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/windows/flutter/dart_ffi - lib = set ${LIB_NAME}.${SDK_EXT} + lib = set ${LIB_NAME}.${LIB_EXT} # copy dll cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \ @@ -150,7 +151,7 @@ script = [ """ echo "🚀 🚀 🚀 Flowy-SDK(linux) build success" dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/linux/flutter/dart_ffi - lib = set lib${LIB_NAME}.${SDK_EXT} + lib = set lib${LIB_NAME}.${LIB_EXT} # copy dll cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \ @@ -163,25 +164,34 @@ script = [ ] script_runner = "@duckscript" -[tasks.copy-to-sys-tmpdir] +[tasks.test-lib-build] +category = "Build" +dependencies = ["env_check"] +run_task = { name = ["setup-test-crate-type","test-sdk-build", "copy-to-sandbox-folder", "restore-test-crate-type"] } + +[tasks.test-sdk-build] +private = true +script = [ + """ + cd rust-lib/ + rustup show + echo cargo build --package=dart-ffi --target ${TEST_COMPILE_TARGET} --features "${FEATURES}" + cargo build --package=dart-ffi --target ${TEST_COMPILE_TARGET} --features "${FEATURES}" + cd ../ + """, +] +script_runner = "@shell" + +[tasks.copy-to-sandbox-folder] private = true script = [ """ # Copy the flowy_sdk lib to system temp directory for flutter unit test. - lib = set lib${LIB_NAME}.${SDK_EXT} + lib = set lib${LIB_NAME}.${TEST_LIB_EXT} dest = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/.sandbox/${lib} rm ${dest} - cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \ + cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${TEST_COMPILE_TARGET}/${TEST_BUILD_FLAG}/${lib} \ ${dest} """, ] script_runner = "@duckscript" - -[tasks.copy-to-sys-tmpdir.windows] -private = true -script = [ - """ - # Doesn't work on windows - """, -] -script_runner = "@duckscript" \ No newline at end of file diff --git a/frontend/scripts/makefile/tool.toml b/frontend/scripts/makefile/tool.toml index f18bb8faa4..08e5de3c49 100644 --- a/frontend/scripts/makefile/tool.toml +++ b/frontend/scripts/makefile/tool.toml @@ -43,7 +43,7 @@ run_task = { name = "remove_files_with_pattern" } #Dart Clean [tasks.rm_dart_generated_files] env = { "dart_flowy_sdk_path" = "./app_flowy/packages/flowy_sdk/" } -run_task = { name = ["rm_dart_generated_protobuf_files"] } +run_task = { name = ["rm_dart_generated_protobuf_files", "rm_dart_generated_event_files"] } [tasks.rm_dart_generated_protobuf_files] private = true @@ -63,6 +63,24 @@ script = [ script_runner = "@duckscript" +[tasks.rm_dart_generated_event_files] +private = true +script = [ + """ + dart_event_folder = glob_array ${dart_flowy_sdk_path}/lib/dispatch/dart_event + + if not array_is_empty ${dart_event_folder} + echo Remove generated dart event files: + for path in ${dart_event_folder} + echo remove ${path} + rm -rf ${path} + end + end + """, +] +script_runner = "@duckscript" + + [tasks.remove_files_with_pattern] private = true script = [ diff --git a/shared-lib/flowy-ast/src/ast.rs b/shared-lib/flowy-ast/src/ast.rs index 73be461b3f..4fec9a5542 100644 --- a/shared-lib/flowy-ast/src/ast.rs +++ b/shared-lib/flowy-ast/src/ast.rs @@ -112,7 +112,7 @@ pub struct ASTField<'a> { } impl<'a> ASTField<'a> { - pub fn new(cx: &Ctxt, field: &'a syn::Field, index: usize) -> Self { + pub fn new(cx: &Ctxt, field: &'a syn::Field, index: usize) -> Result { let mut bracket_inner_ty = None; let mut bracket_ty = None; let mut bracket_category = Some(BracketCategory::Other); @@ -144,15 +144,16 @@ impl<'a> ASTField<'a> { } } Ok(None) => { - cx.error_spanned_by(&field.ty, "fail to get the ty inner type"); + let msg = format!("Fail to get the ty inner type: {:?}", field); + return Err(msg); } Err(e) => { eprintln!("ASTField parser failed: {:?} with error: {}", field, e); - panic!() + return Err(e); } } - ASTField { + Ok(ASTField { member: match &field.ident { Some(ident) => syn::Member::Named(ident.clone()), None => syn::Member::Unnamed(index.into()), @@ -163,7 +164,7 @@ impl<'a> ASTField<'a> { bracket_ty, bracket_inner_ty, bracket_category, - } + }) } pub fn ty_as_str(&self) -> String { @@ -235,6 +236,6 @@ fn fields_from_ast<'a>(cx: &Ctxt, fields: &'a Punctuated) fields .iter() .enumerate() - .map(|(index, field)| ASTField::new(cx, field, index)) + .flat_map(|(index, field)| ASTField::new(cx, field, index).ok()) .collect() } diff --git a/shared-lib/flowy-ast/src/ty_ext.rs b/shared-lib/flowy-ast/src/ty_ext.rs index d3dbd84590..f5beb8b8c7 100644 --- a/shared-lib/flowy-ast/src/ty_ext.rs +++ b/shared-lib/flowy-ast/src/ty_ext.rs @@ -74,8 +74,7 @@ pub fn parse_ty<'a>(ctxt: &Ctxt, ty: &'a syn::Type) -> Result> })); }; } - ctxt.error_spanned_by(ty, "Unsupported inner type, get inner type fail".to_string()); - Ok(None) + Err("Unsupported inner type, get inner type fail".to_string()) } fn parse_bracketed(bracketed: &AngleBracketedGenericArguments) -> Vec<&syn::Type> {