Merge remote-tracking branch 'origin/main' into feature/context_menu

This commit is contained in:
Lucas.Xu 2022-10-18 16:17:29 +08:00
commit 9e40b7f992
53 changed files with 971 additions and 322 deletions

View File

@ -4,8 +4,6 @@ on:
push: push:
branches: branches:
- "main" - "main"
paths:
- "frontend/app_flowy/packages/appflowy_editor/**"
pull_request: pull_request:
branches: branches:

View File

@ -67,7 +67,7 @@ jobs:
- name: Build FlowySDK - name: Build FlowySDK
working-directory: frontend working-directory: frontend
run: | run: |
cargo make --profile development-linux-x86_64 flowy-sdk-dev cargo make --profile test-linux test-lib-build
- name: Code Generation - name: Code Generation
working-directory: frontend/app_flowy working-directory: frontend/app_flowy

View File

@ -118,7 +118,7 @@ jobs:
- name: Archive macOS app - name: Archive macOS app
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }} 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 - name: Upload Release Asset
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1

View File

@ -40,10 +40,15 @@ PRODUCT_NAME = "AppFlowy"
# for cdylib: # for cdylib:
# if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.dylib'); # if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.dylib');
CRATE_TYPE = "staticlib" CRATE_TYPE = "staticlib"
SDK_EXT = "a" LIB_EXT = "a"
APP_ENVIRONMENT = "local" APP_ENVIRONMENT = "local"
FLUTTER_FLOWY_SDK_PATH = "app_flowy/packages/flowy_sdk" FLUTTER_FLOWY_SDK_PATH = "app_flowy/packages/flowy_sdk"
PROTOBUF_DERIVE_CACHE = "../shared-lib/flowy-derive/src/derive_cache/derive_cache.rs" 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] [env.development-mac-arm64]
RUST_LOG = "info" RUST_LOG = "info"
@ -88,7 +93,7 @@ BUILD_FLAG = "debug"
FLUTTER_OUTPUT_DIR = "Debug" FLUTTER_OUTPUT_DIR = "Debug"
PRODUCT_EXT = "exe" PRODUCT_EXT = "exe"
CRATE_TYPE = "cdylib" CRATE_TYPE = "cdylib"
SDK_EXT = "dll" LIB_EXT = "dll"
[env.production-windows-x86] [env.production-windows-x86]
BUILD_FLAG = "release" BUILD_FLAG = "release"
@ -97,7 +102,7 @@ RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
FLUTTER_OUTPUT_DIR = "Release" FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "exe" PRODUCT_EXT = "exe"
CRATE_TYPE = "cdylib" CRATE_TYPE = "cdylib"
SDK_EXT = "dll" LIB_EXT = "dll"
APP_ENVIRONMENT = "production" APP_ENVIRONMENT = "production"
[env.development-linux-x86_64] [env.development-linux-x86_64]
@ -106,7 +111,7 @@ RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
BUILD_FLAG = "debug" BUILD_FLAG = "debug"
CRATE_TYPE = "cdylib" CRATE_TYPE = "cdylib"
FLUTTER_OUTPUT_DIR = "Debug" FLUTTER_OUTPUT_DIR = "Debug"
SDK_EXT = "so" LIB_EXT = "so"
LINUX_ARCH = "x64" LINUX_ARCH = "x64"
[env.production-linux-x86_64] [env.production-linux-x86_64]
@ -115,7 +120,7 @@ TARGET_OS = "linux"
RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu" RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
CRATE_TYPE = "cdylib" CRATE_TYPE = "cdylib"
FLUTTER_OUTPUT_DIR = "Release" FLUTTER_OUTPUT_DIR = "Release"
SDK_EXT = "so" LIB_EXT = "so"
LINUX_ARCH = "x64" LINUX_ARCH = "x64"
APP_ENVIRONMENT = "production" APP_ENVIRONMENT = "production"
@ -125,7 +130,7 @@ RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
BUILD_FLAG = "debug" BUILD_FLAG = "debug"
CRATE_TYPE = "cdylib" CRATE_TYPE = "cdylib"
FLUTTER_OUTPUT_DIR = "Debug" FLUTTER_OUTPUT_DIR = "Debug"
SDK_EXT = "so" LIB_EXT = "so"
LINUX_ARCH = "arm64" LINUX_ARCH = "arm64"
[env.production-linux-aarch64] [env.production-linux-aarch64]
@ -134,7 +139,7 @@ TARGET_OS = "linux"
RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu" RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
CRATE_TYPE = "cdylib" CRATE_TYPE = "cdylib"
FLUTTER_OUTPUT_DIR = "Release" FLUTTER_OUTPUT_DIR = "Release"
SDK_EXT = "so" LIB_EXT = "so"
LINUX_ARCH = "arm64" LINUX_ARCH = "arm64"
APP_ENVIRONMENT = "production" APP_ENVIRONMENT = "production"
@ -197,6 +202,46 @@ script = [
] ]
script_runner = "@duckscript" 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] [tasks.test-build]
condition = { env_set = ["FLUTTER_FLOWY_SDK_PATH"] } condition = { env_set = ["FLUTTER_FLOWY_SDK_PATH"] }
script = [""" script = ["""
@ -204,3 +249,5 @@ script = ["""
cargo build -vv --features=dart cargo build -vv --features=dart
"""] """]
script_runner = "@shell" script_runner = "@shell"

View File

@ -7,7 +7,7 @@
"letsGoButtonText": "Vamos", "letsGoButtonText": "Vamos",
"title": "Título", "title": "Título",
"signUp": { "signUp": {
"buttonText": "Registar", "buttonText": "Registrar",
"title": "Registrar en @:appName", "title": "Registrar en @:appName",
"getStartedText": "Empezar", "getStartedText": "Empezar",
"emptyPasswordError": "La contraseña no puede estar en blanco", "emptyPasswordError": "La contraseña no puede estar en blanco",
@ -16,7 +16,7 @@
"alreadyHaveAnAccount": "¿Posee credenciales?", "alreadyHaveAnAccount": "¿Posee credenciales?",
"emailHint": "Correo", "emailHint": "Correo",
"passwordHint": "Contraseña", "passwordHint": "Contraseña",
"repeatPasswordHint": "Repite la contraseña" "repeatPasswordHint": "Repetir contraseña"
}, },
"signIn": { "signIn": {
"loginTitle": "Ingresa a @:appName", "loginTitle": "Ingresa a @:appName",
@ -58,7 +58,7 @@
} }
}, },
"deletePagePrompt": { "deletePagePrompt": {
"text": "Esta paágina esta en la Papelera", "text": "Esta página está en la Papelera",
"restore": "Recuperar página", "restore": "Recuperar página",
"deletePermanent": "Eliminar permanentemente" "deletePermanent": "Eliminar permanentemente"
}, },
@ -69,7 +69,7 @@
"debug": { "debug": {
"name": "Información de depuración", "name": "Información de depuración",
"success": "¡Información copiada!", "success": "¡Información copiada!",
"fail": "No fué posible copiar la información" "fail": "No fue posible copiar la información"
} }
}, },
"menuAppHeader": { "menuAppHeader": {
@ -167,15 +167,15 @@
"singleSelectFieldName": "Seleccionar", "singleSelectFieldName": "Seleccionar",
"multiSelectFieldName": "Selección múltiple", "multiSelectFieldName": "Selección múltiple",
"urlFieldName": "URL", "urlFieldName": "URL",
"numberFormat": " Formato numérico", "numberFormat": "Formato numérico",
"dateFormat": " Formato de fecha", "dateFormat": "Formato de fecha",
"includeTime": " Incluir tiempo", "includeTime": "Incluir tiempo",
"dateFormatFriendly": "Mes Día, Año", "dateFormatFriendly": "Mes Día, Año",
"dateFormatISO": "Año-Mes-Día", "dateFormatISO": "Año-Mes-Día",
"dateFormatLocal": "Mes/Día/Año", "dateFormatLocal": "Mes/Día/Año",
"dateFormatUS": "Año/Mes/Día", "dateFormatUS": "Año/Mes/Día",
"timeFormat": " Time format", "timeFormat": "Formato de tiempo",
"invalidTimeFormat": "Formato de tiempo", "invalidTimeFormat": "Formato de tiempo inválido",
"timeFormatTwelveHour": "12 horas", "timeFormatTwelveHour": "12 horas",
"timeFormatTwentyFourHour": "24 horas", "timeFormatTwentyFourHour": "24 horas",
"addSelectOption": "Añadir una opción", "addSelectOption": "Añadir una opción",
@ -205,17 +205,22 @@
"panelTitle": "Selecciona una opción o crea una", "panelTitle": "Selecciona una opción o crea una",
"searchOption": "Buscar una opción" "searchOption": "Buscar una opción"
}, },
"menuName": "Grid" "menuName": "Cuadrícula"
}, },
"document": { "document": {
"menuName": "Doc", "menuName": "Documento",
"date": { "date": {
"timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00" "timeHintTextInTwentyFourHour": "13:00"
} }
}, },
"sideBar": { "sideBar": {
"openSidebar": "Open sidebar", "openSidebar": "Abrir panel lateral",
"closeSidebar": "Close sidebar" "closeSidebar": "Cerrar panel lateral"
},
"board": {
"column": {
"create_new_card": "Nuevo"
}
} }
} }

View File

@ -27,7 +27,7 @@ class BoardPluginBuilder implements PluginBuilder {
ViewDataTypePB get dataType => ViewDataTypePB.Database; ViewDataTypePB get dataType => ViewDataTypePB.Database;
@override @override
ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Board; ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board;
} }
class BoardPluginConfig implements PluginConfig { class BoardPluginConfig implements PluginConfig {

View File

@ -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();
// });
// });
// }

View File

@ -80,7 +80,12 @@ class TypeOptionDataController {
Future<void> switchToField(FieldType newFieldType) { Future<void> switchToField(FieldType newFieldType) {
return loader.switchToField(field.id, newFieldType).then((result) { return loader.switchToField(field.id, newFieldType).then((result) {
return result.fold( 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), (err) => Log.error(err),
); );
}); });

View File

@ -12,10 +12,12 @@ import 'grid_data_controller.dart';
import 'row/row_cache.dart'; import 'row/row_cache.dart';
import 'dart:collection'; import 'dart:collection';
import 'row/row_service.dart';
part 'grid_bloc.freezed.dart'; part 'grid_bloc.freezed.dart';
class GridBloc extends Bloc<GridEvent, GridState> { class GridBloc extends Bloc<GridEvent, GridState> {
final GridDataController dataController; final GridDataController dataController;
void Function()? _createRowOperation;
GridBloc({required ViewPB view}) GridBloc({required ViewPB view})
: dataController = GridDataController(view: view), : dataController = GridDataController(view: view),
@ -28,7 +30,19 @@ class GridBloc extends Bloc<GridEvent, GridState> {
await _loadGrid(emit); await _loadGrid(emit);
}, },
createRow: () { 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) { didReceiveGridUpdate: (grid) {
emit(state.copyWith(grid: Some(grid))); emit(state.copyWith(grid: Some(grid)));
@ -84,9 +98,15 @@ class GridBloc extends Bloc<GridEvent, GridState> {
Future<void> _loadGrid(Emitter<GridState> emit) async { Future<void> _loadGrid(Emitter<GridState> emit) async {
final result = await dataController.loadData(); final result = await dataController.loadData();
result.fold( result.fold(
(grid) => emit( (grid) {
state.copyWith(loadingState: GridLoadingState.finish(left(unit))), if (_createRowOperation != null) {
), _createRowOperation?.call();
_createRowOperation = null;
}
emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
);
},
(err) => emit( (err) => emit(
state.copyWith(loadingState: GridLoadingState.finish(right(err))), state.copyWith(loadingState: GridLoadingState.finish(right(err))),
), ),
@ -98,6 +118,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
class GridEvent with _$GridEvent { class GridEvent with _$GridEvent {
const factory GridEvent.initial() = InitialGrid; const factory GridEvent.initial() = InitialGrid;
const factory GridEvent.createRow() = _CreateRow; const factory GridEvent.createRow() = _CreateRow;
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
const factory GridEvent.didReceiveRowUpdate( const factory GridEvent.didReceiveRowUpdate(
List<RowInfo> rows, List<RowInfo> rows,
RowsChangedReason listState, RowsChangedReason listState,

View File

@ -64,6 +64,7 @@ class GridDataController {
}); });
} }
// Loads the rows from each block
Future<Either<Unit, FlowyError>> loadData() async { Future<Either<Unit, FlowyError>> loadData() async {
final result = await _gridFFIService.loadGrid(); final result = await _gridFFIService.loadGrid();
return Future( return Future(

View File

@ -29,7 +29,7 @@ class GridPluginBuilder implements PluginBuilder {
ViewDataTypePB get dataType => ViewDataTypePB.Database; ViewDataTypePB get dataType => ViewDataTypePB.Database;
@override @override
ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Grid; ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Grid;
} }
class GridPluginConfig implements PluginConfig { class GridPluginConfig implements PluginConfig {

View File

@ -1,4 +1,5 @@
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -56,7 +57,6 @@ class CellContainer extends StatelessWidget {
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46), constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus), decoration: _makeBoxDecoration(context, isFocus),
padding: GridSize.cellContentInsets,
child: container, child: container,
), ),
); );
@ -92,8 +92,11 @@ class _GridCellEnterRegion extends StatelessWidget {
builder: (context, onEnter, _) { builder: (context, onEnter, _) {
List<Widget> children = [child]; List<Widget> children = [child];
if (onEnter) { if (onEnter) {
children.add(CellAccessoryContainer(accessories: accessories) children.add(
.positioned(right: 0)); CellAccessoryContainer(accessories: accessories).positioned(
right: GridSize.cellContentInsets.right,
),
);
} }
return MouseRegion( return MouseRegion(

View File

@ -4,6 +4,7 @@ import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
import 'cell_builder.dart'; import 'cell_builder.dart';
class GridCheckboxCell extends GridCellWidget { class GridCheckboxCell extends GridCellWidget {
@ -40,13 +41,16 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
: svgWidget('editor/editor_uncheck'); : svgWidget('editor/editor_uncheck');
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FlowyIconButton( child: Padding(
onPressed: () => context padding: GridSize.cellContentInsets,
.read<CheckboxCellBloc>() child: FlowyIconButton(
.add(const CheckboxCellEvent.select()), onPressed: () => context
iconPadding: EdgeInsets.zero, .read<CheckboxCellBloc>()
icon: icon, .add(const CheckboxCellEvent.select()),
width: 20, iconPadding: EdgeInsets.zero,
icon: icon,
width: 20,
),
), ),
); );
}, },

View File

@ -6,6 +6,7 @@ import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import '../../../layout/sizes.dart';
import '../cell_builder.dart'; import '../cell_builder.dart';
import 'date_editor.dart'; import 'date_editor.dart';
@ -75,7 +76,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
onTap: () => _popover.show(), onTap: () => _popover.show(),
child: Align( child: Align(
alignment: alignment, alignment: alignment,
child: FlowyText.medium(state.dateStr, fontSize: 12), child: Padding(
padding: GridSize.cellContentInsets,
child: FlowyText.medium(state.dateStr, fontSize: 12),
),
), ),
), ),
), ),

View File

@ -4,6 +4,7 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
import 'cell_builder.dart'; import 'cell_builder.dart';
class GridNumberCell extends GridCellWidget { class GridNumberCell extends GridCellWidget {
@ -45,18 +46,21 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
_controller.text = contentFromState(state), _controller.text = contentFromState(state),
), ),
], ],
child: TextField( child: Padding(
controller: _controller, padding: GridSize.cellContentInsets,
focusNode: focusNode, child: TextField(
onEditingComplete: () => focusNode.unfocus(), controller: _controller,
onSubmitted: (_) => focusNode.unfocus(), focusNode: focusNode,
maxLines: 1, onEditingComplete: () => focusNode.unfocus(),
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), onSubmitted: (_) => focusNode.unfocus(),
textInputAction: TextInputAction.done, maxLines: 1,
decoration: const InputDecoration( style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
contentPadding: EdgeInsets.zero, textInputAction: TextInputAction.done,
border: InputBorder.none, decoration: const InputDecoration(
isDense: true, contentPadding: EdgeInsets.zero,
border: InputBorder.none,
isDense: true,
),
), ),
), ),
), ),

View File

@ -11,6 +11,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../layout/sizes.dart';
import '../cell_builder.dart'; import '../cell_builder.dart';
import 'extension.dart'; import 'extension.dart';
import 'select_option_editor.dart'; import 'select_option_editor.dart';
@ -170,7 +171,10 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
_wrapPopover(child), Padding(
padding: GridSize.cellContentInsets,
child: _wrapPopover(child),
),
InkWell(onTap: () => _popover.show()), InkWell(onTap: () => _popover.show()),
], ],
); );

View File

@ -3,6 +3,7 @@ import 'dart:collection';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.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:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
@ -170,10 +171,21 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
.toList(); .toList();
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView( child: ScrollConfiguration(
controller: sc, behavior: ScrollConfiguration.of(context).copyWith(
scrollDirection: Axis.horizontal, dragDevices: {
child: Wrap(spacing: 4, children: children), PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.trackpad,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
},
),
child: SingleChildScrollView(
controller: sc,
scrollDirection: Axis.horizontal,
child: Wrap(spacing: 4, children: children),
),
), ),
); );
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart';
import '../../layout/sizes.dart';
import 'cell_builder.dart'; import 'cell_builder.dart';
class GridTextCellStyle extends GridCellStyle { class GridTextCellStyle extends GridCellStyle {
@ -56,18 +57,21 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
_controller.text = state.content; _controller.text = state.content;
} }
}, },
child: TextField( child: Padding(
controller: _controller, padding: GridSize.cellContentInsets,
focusNode: focusNode, child: TextField(
onChanged: (value) => focusChanged(), controller: _controller,
onEditingComplete: () => focusNode.unfocus(), focusNode: focusNode,
maxLines: null, onChanged: (value) => focusChanged(),
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), onEditingComplete: () => focusNode.unfocus(),
decoration: InputDecoration( maxLines: null,
contentPadding: EdgeInsets.zero, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
border: InputBorder.none, decoration: InputDecoration(
hintText: widget.cellStyle?.placeholder, contentPadding: EdgeInsets.zero,
isDense: true, border: InputBorder.none,
hintText: widget.cellStyle?.placeholder,
isDense: true,
),
), ),
), ),
), ),

View File

@ -12,6 +12,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../layout/sizes.dart';
import '../cell_accessory.dart'; import '../cell_accessory.dart';
import '../cell_builder.dart'; import '../cell_builder.dart';
import 'cell_editor.dart'; import 'cell_editor.dart';
@ -115,14 +116,17 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<URLCellBloc, URLCellState>( child: BlocBuilder<URLCellBloc, URLCellState>(
builder: (context, state) { builder: (context, state) {
final richText = RichText( final richText = Padding(
textAlign: TextAlign.left, padding: GridSize.cellContentInsets,
text: TextSpan( child: RichText(
text: state.content, textAlign: TextAlign.left,
style: TextStyle( text: TextSpan(
color: theme.main2, text: state.content,
fontSize: 14, style: TextStyle(
decoration: TextDecoration.underline, color: theme.main2,
fontSize: 14,
decoration: TextDecoration.underline,
),
), ),
), ),
); );

View File

@ -1,5 +1,6 @@
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart'; 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/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:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show none; import 'package:dartz/dartz.dart' show none;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -58,19 +59,22 @@ class _FieldEditorState extends State<FieldEditor> {
isGroupField: widget.isGroupField, isGroupField: widget.isGroupField,
loader: widget.typeOptionLoader, loader: widget.typeOptionLoader,
)..add(const FieldEditorEvent.initial()), )..add(const FieldEditorEvent.initial()),
child: ListView( child: Padding(
shrinkWrap: true, padding: GridSize.typeOptionContentInsets,
children: [ child: ListView(
FlowyText.medium( shrinkWrap: true,
LocaleKeys.grid_field_editProperty.tr(), children: [
fontSize: 12, FlowyText.medium(
), LocaleKeys.grid_field_editProperty.tr(),
const VSpace(10), fontSize: 12,
_FieldNameTextField(popoverMutex: popoverMutex), ),
const VSpace(10), const VSpace(10),
..._addDeleteFieldButton(), _FieldNameTextField(popoverMutex: popoverMutex),
_FieldTypeOptionCell(popoverMutex: popoverMutex), const VSpace(10),
], ..._addDeleteFieldButton(),
_FieldTypeOptionCell(popoverMutex: popoverMutex),
],
),
), ),
); );
} }

View File

@ -118,11 +118,7 @@ void _resolveFolderDeps(GetIt getIt) {
// AppPB // AppPB
getIt.registerFactoryParam<AppBloc, AppPB, void>( getIt.registerFactoryParam<AppBloc, AppPB, void>(
(app, _) => AppBloc( (app, _) => AppBloc(app: app),
app: app,
appService: AppService(),
appListener: AppListener(appId: app.id),
),
); );
// trash // trash

View File

@ -51,7 +51,7 @@ abstract class PluginBuilder {
ViewDataTypePB get dataType => ViewDataTypePB.Text; ViewDataTypePB get dataType => ViewDataTypePB.Text;
ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Document; ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Document;
} }
abstract class PluginConfig { abstract class PluginConfig {

View File

@ -16,12 +16,12 @@ class InitRustSDKTask extends LaunchTask {
} }
Future<Directory> appFlowyDocumentDirectory() async { Future<Directory> appFlowyDocumentDirectory() async {
Directory documentsDir = await getApplicationDocumentsDirectory();
switch (integrationEnv()) { switch (integrationEnv()) {
case IntegrationMode.develop: case IntegrationMode.develop:
Directory documentsDir = await getApplicationDocumentsDirectory();
return Directory('${documentsDir.path}/flowy_dev').create(); return Directory('${documentsDir.path}/flowy_dev').create();
case IntegrationMode.release: case IntegrationMode.release:
Directory documentsDir = await getApplicationDocumentsDirectory();
return Directory('${documentsDir.path}/flowy').create(); return Directory('${documentsDir.path}/flowy').create();
case IntegrationMode.test: case IntegrationMode.test:
return Directory("${Directory.current.path}/.sandbox"); return Directory("${Directory.current.path}/.sandbox");

View File

@ -22,19 +22,24 @@ class AppBloc extends Bloc<AppEvent, AppState> {
final AppService appService; final AppService appService;
final AppListener appListener; final AppListener appListener;
AppBloc( AppBloc({required this.app})
{required this.app, required this.appService, required this.appListener}) : appService = AppService(),
: super(AppState.initial(app)) { appListener = AppListener(appId: app.id),
super(AppState.initial(app)) {
on<AppEvent>((event, emit) async { on<AppEvent>((event, emit) async {
await event.map(initial: (e) async { await event.map(initial: (e) async {
_startListening(); _startListening();
await _loadViews(emit); await _loadViews(emit);
}, createView: (CreateView value) async { }, createView: (CreateView value) async {
await _createView(value, emit); await _createView(value, emit);
}, loadViews: (_) async {
await _loadViews(emit);
}, didReceiveViewUpdated: (e) async { }, didReceiveViewUpdated: (e) async {
await _didReceiveViewUpdated(e.views, emit); await _didReceiveViewUpdated(e.views, emit);
}, delete: (e) async { }, delete: (e) async {
await _deleteView(emit); await _deleteApp(emit);
}, deleteView: (deletedView) async {
await _deleteView(emit, deletedView.viewId);
}, rename: (e) async { }, rename: (e) async {
await _renameView(e, emit); await _renameView(e, emit);
}, appDidUpdate: (e) async { }, appDidUpdate: (e) async {
@ -71,7 +76,8 @@ class AppBloc extends Bloc<AppEvent, AppState> {
); );
} }
Future<void> _deleteView(Emitter<AppState> emit) async { // Delete the current app
Future<void> _deleteApp(Emitter<AppState> emit) async {
final result = await appService.delete(appId: app.id); final result = await appService.delete(appId: app.id);
result.fold( result.fold(
(unit) => emit(state.copyWith(successOrFailure: left(unit))), (unit) => emit(state.copyWith(successOrFailure: left(unit))),
@ -79,16 +85,24 @@ class AppBloc extends Bloc<AppEvent, AppState> {
); );
} }
Future<void> _deleteView(Emitter<AppState> 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<void> _createView(CreateView value, Emitter<AppState> emit) async { Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
final viewOrFailed = await appService.createView( final result = await appService.createView(
appId: app.id, appId: app.id,
name: value.name, name: value.name,
desc: value.desc, desc: value.desc ?? "",
dataType: value.dataType, dataType: value.pluginBuilder.dataType,
pluginType: value.pluginType, pluginType: value.pluginBuilder.pluginType,
layout: value.layout, layoutType: value.pluginBuilder.layoutType!,
); );
viewOrFailed.fold( result.fold(
(view) => emit(state.copyWith( (view) => emit(state.copyWith(
latestCreatedView: view, latestCreatedView: view,
successOrFailure: left(unit), successOrFailure: left(unit),
@ -107,7 +121,9 @@ class AppBloc extends Bloc<AppEvent, AppState> {
} }
Future<void> _didReceiveViewUpdated( Future<void> _didReceiveViewUpdated(
List<ViewPB> views, Emitter<AppState> emit) async { List<ViewPB> views,
Emitter<AppState> emit,
) async {
final latestCreatedView = state.latestCreatedView; final latestCreatedView = state.latestCreatedView;
AppState newState = state.copyWith(views: views); AppState newState = state.copyWith(views: views);
if (latestCreatedView != null) { if (latestCreatedView != null) {
@ -138,12 +154,12 @@ class AppEvent with _$AppEvent {
const factory AppEvent.initial() = Initial; const factory AppEvent.initial() = Initial;
const factory AppEvent.createView( const factory AppEvent.createView(
String name, String name,
String desc, PluginBuilder pluginBuilder, {
ViewDataTypePB dataType, String? desc,
ViewLayoutTypePB layout, }) = CreateView;
PluginType pluginType, const factory AppEvent.loadViews() = LoadApp;
) = CreateView; const factory AppEvent.delete() = DeleteApp;
const factory AppEvent.delete() = Delete; const factory AppEvent.deleteView(String viewId) = DeleteView;
const factory AppEvent.rename(String newName) = Rename; const factory AppEvent.rename(String newName) = Rename;
const factory AppEvent.didReceiveViewUpdated(List<ViewPB> views) = const factory AppEvent.didReceiveViewUpdated(List<ViewPB> views) =
ReceiveViews; ReceiveViews;
@ -161,7 +177,7 @@ class AppState with _$AppState {
factory AppState.initial(AppPB app) => AppState( factory AppState.initial(AppPB app) => AppState(
app: app, app: app,
views: [], views: app.belongings.items,
successOrFailure: left(unit), successOrFailure: left(unit),
); );
} }

View File

@ -18,17 +18,17 @@ class AppService {
Future<Either<ViewPB, FlowyError>> createView({ Future<Either<ViewPB, FlowyError>> createView({
required String appId, required String appId,
required String name, required String name,
required String desc, String? desc,
required ViewDataTypePB dataType, required ViewDataTypePB dataType,
required PluginType pluginType, required PluginType pluginType,
required ViewLayoutTypePB layout, required ViewLayoutTypePB layoutType,
}) { }) {
var payload = CreateViewPayloadPB.create() var payload = CreateViewPayloadPB.create()
..belongToId = appId ..belongToId = appId
..name = name ..name = name
..desc = desc ..desc = desc ?? ""
..dataType = dataType ..dataType = dataType
..layout = layout; ..layout = layoutType;
return FolderEventCreateView(payload).send(); return FolderEventCreateView(payload).send();
} }
@ -49,6 +49,11 @@ class AppService {
return FolderEventDeleteApp(request).send(); return FolderEventDeleteApp(request).send();
} }
Future<Either<Unit, FlowyError>> deleteView({required String viewId}) {
final request = RepeatedViewIdPB.create()..items.add(viewId);
return FolderEventDeleteView(request).send();
}
Future<Either<Unit, FlowyError>> updateApp( Future<Either<Unit, FlowyError>> updateApp(
{required String appId, String? name}) { {required String appId, String? name}) {
UpdateAppPayloadPB payload = UpdateAppPayloadPB.create()..appId = appId; UpdateAppPayloadPB payload = UpdateAppPayloadPB.create()..appId = appId;

View File

@ -31,12 +31,14 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
}, },
viewDidUpdate: (e) { viewDidUpdate: (e) {
e.result.fold( 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))), (error) => emit(state.copyWith(successOrFailure: right(error))),
); );
}, },
rename: (e) async { 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( emit(
result.fold( result.fold(
(l) => state.copyWith(successOrFailure: left(unit)), (l) => state.copyWith(successOrFailure: left(unit)),
@ -46,7 +48,6 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
}, },
delete: (e) async { delete: (e) async {
final result = await service.delete(viewId: view.id); final result = await service.delete(viewId: view.id);
await service.updateView(viewId: view.id);
emit( emit(
result.fold( result.fold(
(l) => state.copyWith(successOrFailure: left(unit)), (l) => state.copyWith(successOrFailure: left(unit)),
@ -81,7 +82,8 @@ class ViewEvent with _$ViewEvent {
const factory ViewEvent.rename(String newName) = Rename; const factory ViewEvent.rename(String newName) = Rename;
const factory ViewEvent.delete() = Delete; const factory ViewEvent.delete() = Delete;
const factory ViewEvent.duplicate() = Duplicate; const factory ViewEvent.duplicate() = Duplicate;
const factory ViewEvent.viewDidUpdate(Either<ViewPB, FlowyError> result) = ViewDidUpdate; const factory ViewEvent.viewDidUpdate(Either<ViewPB, FlowyError> result) =
ViewDidUpdate;
} }
@freezed @freezed

View File

@ -17,11 +17,11 @@ class WorkspaceService {
required this.workspaceId, required this.workspaceId,
}); });
Future<Either<AppPB, FlowyError>> createApp( Future<Either<AppPB, FlowyError>> createApp(
{required String name, required String desc}) { {required String name, String? desc}) {
final payload = CreateAppPayloadPB.create() final payload = CreateAppPayloadPB.create()
..name = name ..name = name
..workspaceId = workspaceId ..workspaceId = workspaceId
..desc = desc; ..desc = desc ?? "";
return FolderEventCreateApp(payload).send(); return FolderEventCreateApp(payload).send();
} }

View File

@ -104,13 +104,12 @@ class MenuAppHeader extends StatelessWidget {
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(), message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
child: AddButton( child: AddButton(
onSelected: (pluginBuilder) { onSelected: (pluginBuilder) {
context.read<AppBloc>().add(AppEvent.createView( context.read<AppBloc>().add(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(), AppEvent.createView(
"", LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
pluginBuilder.dataType, pluginBuilder,
pluginBuilder.subDataType!, ),
pluginBuilder.pluginType, );
));
}, },
).padding(right: MenuAppSizes.headerPadding), ).padding(right: MenuAppSizes.headerPadding),
); );

View File

@ -50,7 +50,6 @@ class _MenuAppState extends State<MenuApp> {
}, },
), ),
BlocListener<AppBloc, AppState>( BlocListener<AppBloc, AppState>(
listenWhen: (p, c) => p.views != c.views,
listener: (context, state) => viewDataContext.views = state.views, listener: (context, state) => viewDataContext.views = state.views,
), ),
], ],

View File

@ -188,9 +188,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
return KeyEventResult.handled; return KeyEventResult.handled;
}; };
/// To create a link, enclose the link text in brackets (e.g., [link text]). ShortcutEventHandler markdownLinkOrImageHandler = (editorState, event) {
/// Then, immediately follow it with the URL in parentheses (e.g., (https://example.com)).
ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
final selectionService = editorState.service.selectionService; final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value; final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>(); final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
@ -198,48 +196,72 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
return KeyEventResult.ignored; 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 textNode = textNodes.first;
final text = textNode.toPlainText(); final text = textNode.toPlainText();
final firstExclamation = text.indexOf('!');
final firstOpeningBracket = text.indexOf('['); final firstOpeningBracket = text.indexOf('[');
final firstClosingBracket = text.indexOf(']'); final firstClosingBracket = text.indexOf(']');
// use regex to validate the format of the link // Use RegEx to determine whether it's an image or a link
// note: this enforces that the link has http or https // Difference between image and link syntax is that image
final regexp = RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d./?=#]+)$'); // has an exclamation point at the beginning.
final match = regexp.firstMatch(text); // Note: The RegEx enforces that the URL has http or https
if (match == null) { 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; 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; return KeyEventResult.handled;
}; };
@ -369,6 +391,5 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
), ),
); );
editorState.apply(transaction); editorState.apply(transaction);
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -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:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
//
List<ShortcutEvent> builtInShortcutEvents = [ List<ShortcutEvent> builtInShortcutEvents = [
ShortcutEvent( ShortcutEvent(
key: 'Move cursor up', key: 'Move cursor up',
@ -276,9 +275,9 @@ List<ShortcutEvent> builtInShortcutEvents = [
handler: doubleTildeToStrikethrough, handler: doubleTildeToStrikethrough,
), ),
ShortcutEvent( ShortcutEvent(
key: 'Markdown link to link', key: 'Markdown link or image',
command: 'shift+parenthesis right', command: 'shift+parenthesis right',
handler: markdownLinkToLinkHandler, handler: markdownLinkOrImageHandler,
), ),
// https://github.com/flutter/flutter/issues/104944 // https://github.com/flutter/flutter/issues/104944
// Workaround: Using space editing on the web platform often results in errors, // Workaround: Using space editing on the web platform often results in errors,

View File

@ -7,10 +7,10 @@ import 'package:ffi/ffi.dart' as ffi;
import 'package:flutter/foundation.dart' as Foundation; import 'package:flutter/foundation.dart' as Foundation;
// ignore_for_file: unused_import, camel_case_types, non_constant_identifier_names // 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 /// 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() { DynamicLibrary _open() {
if (Platform.environment.containsKey('FLUTTER_TEST')) { if (Platform.environment.containsKey('FLUTTER_TEST')) {
final prefix = "${Directory.current.path}/.sandbox"; final prefix = "${Directory.current.path}/.sandbox";
@ -18,7 +18,8 @@ DynamicLibrary _open() {
return DynamicLibrary.open('${prefix}/libdart_ffi.so'); return DynamicLibrary.open('${prefix}/libdart_ffi.so');
if (Platform.isAndroid) if (Platform.isAndroid)
return DynamicLibrary.open('${prefix}/libdart_ffi.so'); 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.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a');
if (Platform.isWindows) if (Platform.isWindows)
return DynamicLibrary.open('${prefix}/dart_ffi.dll'); return DynamicLibrary.open('${prefix}/dart_ffi.dll');
@ -42,8 +43,8 @@ void async_event(
_invoke_async(port, input, len); _invoke_async(port, input, len);
} }
final _invoke_async_Dart _invoke_async = final _invoke_async_Dart _invoke_async = _dart_ffi_lib
_dl.lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event'); .lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event');
typedef _invoke_async_C = Void Function( typedef _invoke_async_C = Void Function(
Int64 port, Int64 port,
Pointer<Uint8> input, Pointer<Uint8> input,
@ -63,8 +64,8 @@ Pointer<Uint8> sync_event(
return _invoke_sync(input, len); return _invoke_sync(input, len);
} }
final _invoke_sync_Dart _invoke_sync = final _invoke_sync_Dart _invoke_sync = _dart_ffi_lib
_dl.lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event'); .lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event');
typedef _invoke_sync_C = Pointer<Uint8> Function( typedef _invoke_sync_C = Pointer<Uint8> Function(
Pointer<Uint8> input, Pointer<Uint8> input,
Uint64 len, Uint64 len,
@ -82,7 +83,7 @@ int init_sdk(
} }
final _init_sdk_Dart _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( typedef _init_sdk_C = Int64 Function(
Pointer<ffi.Utf8> path, Pointer<ffi.Utf8> path,
); );
@ -96,7 +97,7 @@ int set_stream_port(int port) {
} }
final _set_stream_port_Dart _set_stream_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'); 'set_stream_port');
typedef _set_stream_port_C = Int32 Function( typedef _set_stream_port_C = Int32 Function(
@ -111,7 +112,7 @@ void link_me_please() {
_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'); .lookupFunction<_link_me_please_C, _link_me_please_Dart>('link_me_please');
typedef _link_me_please_C = Void Function(); typedef _link_me_please_C = Void Function();
typedef _link_me_please_Dart = void Function(); typedef _link_me_please_Dart = void Function();
@ -123,7 +124,7 @@ void store_dart_post_cobject(
_store_dart_post_cobject(ptr); _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>( .lookupFunction<_store_dart_post_cobject_C, _store_dart_post_cobject_Dart>(
'store_dart_post_cobject'); 'store_dart_post_cobject');
typedef _store_dart_post_cobject_C = Void Function( typedef _store_dart_post_cobject_C = Void Function(

View File

@ -49,7 +49,7 @@ packages:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.1" version: "3.1.11"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -245,7 +245,7 @@ packages:
name: coverage name: coverage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.2" version: "1.2.0"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -259,7 +259,7 @@ packages:
name: crypto name: crypto
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.0.1"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@ -454,6 +454,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.1" version: "0.6.1"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_inappwebview: flutter_inappwebview:
dependency: transitive dependency: transitive
description: description:
@ -555,6 +560,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
get_it: get_it:
dependency: "direct main" dependency: "direct main"
description: description:
@ -660,6 +670,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.5.0" version: "2.5.0"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1246,6 +1261,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.1+2" 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: table_calendar:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1322,7 +1344,7 @@ packages:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.0"
universal_platform: universal_platform:
dependency: transitive dependency: transitive
description: description:
@ -1441,7 +1463,7 @@ packages:
name: vm_service name: vm_service
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.3.0" version: "8.2.2"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -1456,6 +1478,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
webdriver:
dependency: transitive
description:
name: webdriver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
webkit_inspection_protocol: webkit_inspection_protocol:
dependency: transitive dependency: transitive
description: description:

View File

@ -97,6 +97,8 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
integration_test:
sdk: flutter
build_runner: ^2.2.0 build_runner: ^2.2.0
freezed: ^2.1.0+1 freezed: ^2.1.0+1
bloc_test: ^9.0.2 bloc_test: ^9.0.2

View File

@ -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<GridBloc, GridState>(
"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);
});
});
}

View File

@ -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<SelectOptionCellEditorBloc, SelectOptionEditorState>(
"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");
},
);
});
}

View File

@ -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<AppFlowyGridTest> ensureInitialized() async {
final inner = await AppFlowyUnitTest.ensureInitialized();
final test = AppFlowyGridTest(inner);
await test._createTestGrid();
return test;
}
Future<void> _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<AppFlowyGridSelectOptionCellTest> 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<GridSelectOptionCellController> 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<AppFlowyGridCellTest> ensureInitialized() async {
final gridTest = await AppFlowyGridTest.ensureInitialized();
final test = AppFlowyGridCellTest(gridTest);
await test._loadGridData();
return test;
}
Future<void> _loadGridData() async {
final result = await _dataController.loadData();
result.fold((l) => null, (r) => throw Exception(r));
}
Future<GridCellControllerBuilder> 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<void> gridResponseFuture({int milliseconds = 200}) {
return Future.delayed(gridResponseDuration(milliseconds: milliseconds));
}
Duration gridResponseDuration({int milliseconds = 200}) {
return Duration(milliseconds: milliseconds);
}

View File

@ -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<AppBloc, AppState>(
"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<AppBloc, AppState>(
"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<AppBloc, AppState>(
"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<AppBloc, AppState>(
"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<AppBloc, AppState>(
"delete the document",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) => bloc.add(AppEvent.deleteView(view.id)),
);
blocTest<AppBloc, AppState>(
"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);
},
);
});
}

View File

@ -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<AppFlowyIntegrateTest> ensureInitialized() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
main();
return AppFlowyIntegrateTest();
}
}
class AppFlowyUnitTest {
late UserProfilePB userProfile;
late UserService userService;
late WorkspaceService workspaceService;
late List<WorkspacePB> workspaces;
static Future<AppFlowyUnitTest> 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<void> _signIn() async {
final authService = getIt<AuthService>();
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<void> _loadWorkspace() async {
final result = await userService.getWorkspaces();
result.fold(
(value) => workspaces = value,
(error) {
throw Exception(error);
},
);
}
Future<void> _initialServices() async {
workspaceService = WorkspaceService(workspaceId: currentWorkspace.id);
}
Future<AppPB> 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);
}

View File

@ -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<FlowyTest> setup() async {
TestWidgetsFlutterBinding.ensureInitialized();
// await EasyLocalization.ensureInitialized();
await FlowyRunner.run(FlowyTestApp());
return FlowyTest();
}
Future<UserProfilePB> signIn() async {
final authService = getIt<AuthService>();
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();
}
}

View File

@ -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<WelcomeBloc, WelcomeState>(
"welcome screen init",
build: () => getIt<WelcomeBloc>(param1: userInfo),
act: (bloc) {
bloc.add(const WelcomeEvent.initial());
},
wait: const Duration(seconds: 3),
verify: (bloc) {
assert(bloc.state.isLoading == false);
},
);
});
}

View File

@ -93,10 +93,9 @@ impl GridManager {
Ok(()) Ok(())
} }
#[tracing::instrument(level = "debug", skip_all, fields(grid_id), err)] #[tracing::instrument(level = "debug", skip_all, err)]
pub async fn open_grid<T: AsRef<str>>(&self, grid_id: T) -> FlowyResult<Arc<GridRevisionEditor>> { pub async fn open_grid<T: AsRef<str>>(&self, grid_id: T) -> FlowyResult<Arc<GridRevisionEditor>> {
let grid_id = grid_id.as_ref(); let grid_id = grid_id.as_ref();
tracing::Span::current().record("grid_id", &grid_id);
let _ = self.migration.run_v1_migration(grid_id).await; let _ = self.migration.run_v1_migration(grid_id).await;
self.get_or_create_grid_editor(grid_id).await self.get_or_create_grid_editor(grid_id).await
} }

View File

@ -1,6 +1,7 @@
mod multi_select_type_option; mod multi_select_type_option;
mod select_type_option; mod select_type_option;
mod single_select_type_option; mod single_select_type_option;
mod type_option_transform;
pub use multi_select_type_option::*; pub use multi_select_type_option::*;
pub use select_type_option::*; pub use select_type_option::*;

View File

@ -1,6 +1,7 @@
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::impl_type_option; use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; 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::type_options::util::get_cell_data;
use crate::services::field::{ use crate::services::field::{
BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction, BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction,
@ -110,7 +111,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
} }
fn transform(&mut self, field_type: &FieldType, type_option_data: String) { 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)] #[cfg(test)]

View File

@ -2,7 +2,8 @@ use crate::entities::{CellChangesetPB, FieldType, GridCellIdPB, GridCellIdParams
use crate::services::cell::{ use crate::services::cell::{
CellBytes, CellBytesParser, CellData, CellDataIsEmpty, CellDisplayable, FromCellChangeset, FromCellString, 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 bytes::Bytes;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyResult}; 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( fn transform_cell_data(
&self, &self,
cell_data: CellData<SelectOptionIds>, cell_data: CellData<SelectOptionIds>,
@ -150,6 +132,21 @@ pub trait SelectTypeOptionSharedAction: TypeOptionDataSerializer + Send + Sync {
} }
FieldType::Checkbox => { FieldType::Checkbox => {
// transform the cell data to the option id // 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()); return Ok(CellBytes::default());
@ -174,7 +171,12 @@ where
decoded_field_type: &FieldType, decoded_field_type: &FieldType,
field_rev: &FieldRevision, field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> { ) -> FlowyResult<CellBytes> {
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( fn displayed_cell_string(

View File

@ -1,6 +1,7 @@
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::impl_type_option; use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; 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::{BoxTypeOptionBuilder, TypeOptionBuilder};
use crate::services::field::{ use crate::services::field::{
SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction, SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction,
@ -96,7 +97,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
} }
fn transform(&mut self, field_type: &FieldType, type_option_data: String) { 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)
} }
} }

View File

@ -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<T>(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<T>(
shared: &T,
cell_data: CellData<SelectOptionIds>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes>
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()),
}
}
}

View File

@ -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)] #[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)]
pub struct RichTextTypeOptionPB { pub struct RichTextTypeOptionPB {
#[pb(index = 1)] #[pb(index = 1)]
#[serde(default)] #[serde(default)]
data: String, //It's not used yet data: String,
} }
impl_type_option!(RichTextTypeOptionPB, FieldType::RichText); impl_type_option!(RichTextTypeOptionPB, FieldType::RichText);

View File

@ -100,7 +100,8 @@ impl GridRevisionEditor {
/// ///
/// * `grid_id`: the id of the grid /// * `grid_id`: the id of the grid
/// * `field_id`: the id of the field /// * `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( pub async fn update_field_type_option(
&self, &self,
@ -108,7 +109,6 @@ impl GridRevisionEditor {
field_id: &str, field_id: &str,
type_option_data: Vec<u8>, type_option_data: Vec<u8>,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
debug_assert!(!type_option_data.is_empty());
if type_option_data.is_empty() { if type_option_data.is_empty() {
return Ok(()); return Ok(());
} }

View File

@ -21,17 +21,18 @@ run_task = { name = ["setup-crate-type","sdk-build-android", "restore-crate-type
[tasks.flowy-sdk-dev-macos] [tasks.flowy-sdk-dev-macos]
category = "Build" category = "Build"
dependencies = ["env_check"] 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] [tasks.flowy-sdk-dev-windows]
category = "Build" category = "Build"
dependencies = ["env_check"] 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] [tasks.flowy-sdk-dev-linux]
category = "Build" category = "Build"
dependencies = ["env_check"] 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] [tasks.sdk-build]
@ -114,7 +115,7 @@ script = [
""" """
echo "🚀 🚀 🚀 Flowy-SDK(macOS) build success" echo "🚀 🚀 🚀 Flowy-SDK(macOS) build success"
dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/packages/flowy_sdk/${TARGET_OS} 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} \ cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
${dart_ffi_dir}/${lib} ${dart_ffi_dir}/${lib}
@ -131,7 +132,7 @@ script = [
""" """
echo "🚀 🚀 🚀 Flowy-SDK(windows) build success" echo "🚀 🚀 🚀 Flowy-SDK(windows) build success"
dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/windows/flutter/dart_ffi 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 # copy dll
cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \ 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" echo "🚀 🚀 🚀 Flowy-SDK(linux) build success"
dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/linux/flutter/dart_ffi 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 # copy dll
cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \ cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
@ -163,25 +164,34 @@ script = [
] ]
script_runner = "@duckscript" 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 private = true
script = [ script = [
""" """
# Copy the flowy_sdk lib to system temp directory for flutter unit test. # 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} dest = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/.sandbox/${lib}
rm ${dest} 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} ${dest}
""", """,
] ]
script_runner = "@duckscript" script_runner = "@duckscript"
[tasks.copy-to-sys-tmpdir.windows]
private = true
script = [
"""
# Doesn't work on windows
""",
]
script_runner = "@duckscript"

View File

@ -43,7 +43,7 @@ run_task = { name = "remove_files_with_pattern" }
#Dart Clean #Dart Clean
[tasks.rm_dart_generated_files] [tasks.rm_dart_generated_files]
env = { "dart_flowy_sdk_path" = "./app_flowy/packages/flowy_sdk/" } 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] [tasks.rm_dart_generated_protobuf_files]
private = true private = true
@ -63,6 +63,24 @@ script = [
script_runner = "@duckscript" 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] [tasks.remove_files_with_pattern]
private = true private = true
script = [ script = [

View File

@ -112,7 +112,7 @@ pub struct ASTField<'a> {
} }
impl<'a> 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<Self, String> {
let mut bracket_inner_ty = None; let mut bracket_inner_ty = None;
let mut bracket_ty = None; let mut bracket_ty = None;
let mut bracket_category = Some(BracketCategory::Other); let mut bracket_category = Some(BracketCategory::Other);
@ -144,15 +144,16 @@ impl<'a> ASTField<'a> {
} }
} }
Ok(None) => { 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) => { Err(e) => {
eprintln!("ASTField parser failed: {:?} with error: {}", field, e); eprintln!("ASTField parser failed: {:?} with error: {}", field, e);
panic!() return Err(e);
} }
} }
ASTField { Ok(ASTField {
member: match &field.ident { member: match &field.ident {
Some(ident) => syn::Member::Named(ident.clone()), Some(ident) => syn::Member::Named(ident.clone()),
None => syn::Member::Unnamed(index.into()), None => syn::Member::Unnamed(index.into()),
@ -163,7 +164,7 @@ impl<'a> ASTField<'a> {
bracket_ty, bracket_ty,
bracket_inner_ty, bracket_inner_ty,
bracket_category, bracket_category,
} })
} }
pub fn ty_as_str(&self) -> String { pub fn ty_as_str(&self) -> String {
@ -235,6 +236,6 @@ fn fields_from_ast<'a>(cx: &Ctxt, fields: &'a Punctuated<syn::Field, Token![,]>)
fields fields
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, field)| ASTField::new(cx, field, index)) .flat_map(|(index, field)| ASTField::new(cx, field, index).ok())
.collect() .collect()
} }

View File

@ -74,8 +74,7 @@ pub fn parse_ty<'a>(ctxt: &Ctxt, ty: &'a syn::Type) -> Result<Option<TyInfo<'a>>
})); }));
}; };
} }
ctxt.error_spanned_by(ty, "Unsupported inner type, get inner type fail".to_string()); Err("Unsupported inner type, get inner type fail".to_string())
Ok(None)
} }
fn parse_bracketed(bracketed: &AngleBracketedGenericArguments) -> Vec<&syn::Type> { fn parse_bracketed(bracketed: &AngleBracketedGenericArguments) -> Vec<&syn::Type> {