mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge remote-tracking branch 'origin/main' into feature/theme
This commit is contained in:
commit
5c13b324ec
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,5 +1,27 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.0.5.1 - 09/14/2022
|
||||
|
||||
New features
|
||||
- Enable deleting a field in board
|
||||
- Fix some bugs
|
||||
|
||||
|
||||
## Version 0.0.5 - 09/08/2022
|
||||
New Features - Kanban Board like Notion and Trello beta
|
||||
Boards are the best way to manage projects & tasks. Use them to group your databases by select, multiselect, and checkbox.
|
||||
|
||||
<p align="left"><img src="https://user-images.githubusercontent.com/12026239/190055984-6efa2d7a-ee38-4551-859e-ee56388e1859.gif" width="1000px" /></p>
|
||||
|
||||
- Set up columns that represent a specific phase of the project cycle and use cards to represent each project / task
|
||||
- Drag and drop a card from one phase / column to another phase / column
|
||||
- Update database properties in the Board view by clicking on a property and making edits on the card
|
||||
|
||||
### Other Features & Improvements
|
||||
- Settings allow users to change avatars
|
||||
- Click and drag the right edge to resize your sidebar
|
||||
- And many user interface improvements (link)
|
||||
|
||||
## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022
|
||||
|
||||
New features
|
||||
|
@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.0.5"
|
||||
CURRENT_APP_VERSION = "0.0.5.1"
|
||||
FEATURES = "flutter"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
@ -191,7 +191,8 @@
|
||||
"optionTitle": "Options",
|
||||
"addOption": "Add option",
|
||||
"editProperty": "Edit property",
|
||||
"newColumn": "New column"
|
||||
"newColumn": "New column",
|
||||
"deleteFieldPromptMessage": "Are you sure? This property will be deleted"
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "Duplicate",
|
||||
|
@ -287,8 +287,13 @@ class _BoardContentState extends State<BoardContent> {
|
||||
);
|
||||
}
|
||||
|
||||
void _openCard(String gridId, GridFieldController fieldController,
|
||||
RowPB rowPB, GridRowCache rowCache, BuildContext context) {
|
||||
void _openCard(
|
||||
String gridId,
|
||||
GridFieldController fieldController,
|
||||
RowPB rowPB,
|
||||
GridRowCache rowCache,
|
||||
BuildContext context,
|
||||
) {
|
||||
final rowInfo = RowInfo(
|
||||
gridId: gridId,
|
||||
fields: UnmodifiableListView(fieldController.fieldContexts),
|
||||
|
@ -2,11 +2,12 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_group.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@ -141,10 +142,12 @@ extension _GridSettingExtension on BoardSettingAction {
|
||||
}
|
||||
|
||||
class BoardSettingListPopover extends StatefulWidget {
|
||||
final PopoverController popoverController;
|
||||
final BoardSettingContext settingContext;
|
||||
|
||||
const BoardSettingListPopover({
|
||||
Key? key,
|
||||
required this.popoverController,
|
||||
required this.settingContext,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -153,36 +156,33 @@ class BoardSettingListPopover extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
|
||||
bool _showGridPropertyList = false;
|
||||
BoardSettingAction? _action;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_showGridPropertyList) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
child: GridPropertyList(
|
||||
gridId: widget.settingContext.viewId,
|
||||
fieldController: widget.settingContext.fieldController,
|
||||
),
|
||||
);
|
||||
if (_action != null) {
|
||||
switch (_action!) {
|
||||
case BoardSettingAction.groups:
|
||||
return GridGroupList(
|
||||
viewId: widget.settingContext.viewId,
|
||||
fieldController: widget.settingContext.fieldController,
|
||||
onDismissed: () {
|
||||
widget.popoverController.close();
|
||||
},
|
||||
);
|
||||
case BoardSettingAction.properties:
|
||||
return GridPropertyList(
|
||||
gridId: widget.settingContext.viewId,
|
||||
fieldController: widget.settingContext.fieldController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(140, 400)),
|
||||
child: BoardSettingList(
|
||||
settingContext: widget.settingContext,
|
||||
onAction: (action, settingContext) {
|
||||
switch (action) {
|
||||
case BoardSettingAction.groups:
|
||||
break;
|
||||
case BoardSettingAction.properties:
|
||||
setState(() {
|
||||
_showGridPropertyList = true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
return BoardSettingList(
|
||||
settingContext: widget.settingContext,
|
||||
onAction: (action, settingContext) {
|
||||
setState(() => _action = action);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -40,15 +41,30 @@ class BoardToolbar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingButton extends StatelessWidget {
|
||||
class _SettingButton extends StatefulWidget {
|
||||
final BoardSettingContext settingContext;
|
||||
const _SettingButton({required this.settingContext, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<_SettingButton> createState() => _SettingButtonState();
|
||||
}
|
||||
|
||||
class _SettingButtonState extends State<_SettingButton> {
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
child: FlowyIconButton(
|
||||
hoverColor: theme.hover,
|
||||
@ -61,7 +77,8 @@ class _SettingButton extends StatelessWidget {
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return BoardSettingListPopover(
|
||||
settingContext: settingContext,
|
||||
settingContext: widget.settingContext,
|
||||
popoverController: popoverController,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -90,7 +90,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final result = await service.openDocument(docId: view.id);
|
||||
result.fold(
|
||||
(block) {
|
||||
document = _decodeJsonToDocument(block.deltaStr);
|
||||
document = _decodeJsonToDocument(block.snapshot);
|
||||
_subscription = document.changes.listen((event) {
|
||||
final delta = event.item2;
|
||||
final documentDelta = document.toDelta();
|
||||
@ -115,16 +115,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
void _composeDelta(Delta composedDelta, Delta documentDelta) async {
|
||||
final json = jsonEncode(composedDelta.toJson());
|
||||
Log.debug("doc_id: $view.id - Send json: $json");
|
||||
final result = await service.composeDelta(docId: view.id, data: json);
|
||||
final result = await service.applyEdit(docId: view.id, data: json);
|
||||
|
||||
result.fold((rustDoc) {
|
||||
// final json = utf8.decode(doc.data);
|
||||
final rustDelta = Delta.fromJson(jsonDecode(rustDoc.deltaStr));
|
||||
if (documentDelta != rustDelta) {
|
||||
Log.error("Receive : $rustDelta");
|
||||
Log.error("Expected : $documentDelta");
|
||||
}
|
||||
}, (r) => null);
|
||||
result.fold(
|
||||
(_) {},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
}
|
||||
|
||||
Document _decodeJsonToDocument(String data) {
|
||||
|
@ -4,22 +4,28 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-sync/text_block.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-text-block/entities.pb.dart';
|
||||
|
||||
class DocumentService {
|
||||
Future<Either<TextBlockDeltaPB, FlowyError>> openDocument({
|
||||
Future<Either<TextBlockPB, FlowyError>> openDocument({
|
||||
required String docId,
|
||||
}) async {
|
||||
await FolderEventSetLatestView(ViewIdPB(value: docId)).send();
|
||||
|
||||
final payload = TextBlockIdPB(value: docId);
|
||||
return TextBlockEventGetBlockData(payload).send();
|
||||
return TextBlockEventGetTextBlock(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<TextBlockDeltaPB, FlowyError>> composeDelta({required String docId, required String data}) {
|
||||
final payload = TextBlockDeltaPB.create()
|
||||
..blockId = docId
|
||||
..deltaStr = data;
|
||||
return TextBlockEventApplyDelta(payload).send();
|
||||
Future<Either<Unit, FlowyError>> applyEdit({
|
||||
required String docId,
|
||||
required String data,
|
||||
String operations = "",
|
||||
}) {
|
||||
final payload = EditPayloadPB.create()
|
||||
..textBlockId = docId
|
||||
..operations = operations
|
||||
..delta = data;
|
||||
return TextBlockEventApplyEdit(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {
|
||||
|
@ -186,7 +186,8 @@ class DocumentShareButton extends StatelessWidget {
|
||||
'Exported to: ${LocaleKeys.notifications_export_path.tr()}');
|
||||
break;
|
||||
case ShareAction.copyLink:
|
||||
FlowyAlertDialog(title: LocaleKeys.shareAction_workInProgress.tr())
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.shareAction_workInProgress.tr())
|
||||
.show(context);
|
||||
break;
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ class FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
|
||||
value = values.first;
|
||||
}
|
||||
|
||||
TextFieldDialog(
|
||||
NavigatorTextFieldDialog(
|
||||
title: 'URL',
|
||||
value: value,
|
||||
confirm: (newValue) {
|
||||
|
@ -2,6 +2,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'field_service.dart';
|
||||
import 'type_option/type_option_context.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@ -15,10 +16,11 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
FieldEditorBloc({
|
||||
required String gridId,
|
||||
required String fieldName,
|
||||
required bool isGroupField,
|
||||
required IFieldTypeOptionLoader loader,
|
||||
}) : dataController =
|
||||
TypeOptionDataController(gridId: gridId, loader: loader),
|
||||
super(FieldEditorState.initial(gridId, fieldName)) {
|
||||
super(FieldEditorState.initial(gridId, fieldName, isGroupField)) {
|
||||
on<FieldEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@ -35,7 +37,23 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
emit(state.copyWith(name: name));
|
||||
},
|
||||
didReceiveFieldChanged: (FieldPB field) {
|
||||
emit(state.copyWith(field: Some(field), name: field.name));
|
||||
emit(state.copyWith(
|
||||
field: Some(field),
|
||||
name: field.name,
|
||||
canDelete: field.isPrimary,
|
||||
));
|
||||
},
|
||||
deleteField: () {
|
||||
state.field.fold(
|
||||
() => null,
|
||||
(field) {
|
||||
final fieldService = FieldService(
|
||||
gridId: gridId,
|
||||
fieldId: field.id,
|
||||
);
|
||||
fieldService.deleteField();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -52,6 +70,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
class FieldEditorEvent with _$FieldEditorEvent {
|
||||
const factory FieldEditorEvent.initial() = _InitialField;
|
||||
const factory FieldEditorEvent.updateName(String name) = _UpdateName;
|
||||
const factory FieldEditorEvent.deleteField() = _DeleteField;
|
||||
const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
|
||||
_DidReceiveFieldChanged;
|
||||
}
|
||||
@ -63,16 +82,21 @@ class FieldEditorState with _$FieldEditorState {
|
||||
required String errorText,
|
||||
required String name,
|
||||
required Option<FieldPB> field,
|
||||
required bool canDelete,
|
||||
required bool isGroupField,
|
||||
}) = _FieldEditorState;
|
||||
|
||||
factory FieldEditorState.initial(
|
||||
String gridId,
|
||||
String fieldName,
|
||||
bool isGroupField,
|
||||
) =>
|
||||
FieldEditorState(
|
||||
gridId: gridId,
|
||||
errorText: '',
|
||||
field: none(),
|
||||
canDelete: false,
|
||||
name: fieldName,
|
||||
isGroupField: isGroupField,
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@ -24,6 +25,13 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
didReceiveCellDatas: (_DidReceiveCellDatas value) {
|
||||
emit(state.copyWith(gridCells: value.gridCells));
|
||||
},
|
||||
deleteField: (_DeleteField value) {
|
||||
final fieldService = FieldService(
|
||||
gridId: dataController.rowInfo.gridId,
|
||||
fieldId: value.fieldId,
|
||||
);
|
||||
fieldService.deleteField();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -49,6 +57,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
@freezed
|
||||
class RowDetailEvent with _$RowDetailEvent {
|
||||
const factory RowDetailEvent.initial() = _Initial;
|
||||
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
|
||||
const factory RowDetailEvent.didReceiveCellDatas(
|
||||
List<GridCellIdentifier> gridCells) = _DidReceiveCellDatas;
|
||||
}
|
||||
|
@ -317,9 +317,7 @@ class _GridFooter extends StatelessWidget {
|
||||
height: GridSize.footerHeight,
|
||||
child: Padding(
|
||||
padding: GridSize.footerContentInsets,
|
||||
child: const Expanded(
|
||||
child: SizedBox(height: 40, child: GridAddRowButton()),
|
||||
),
|
||||
child: const SizedBox(height: 40, child: GridAddRowButton()),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -62,10 +63,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||
builder: (context, state) {
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
controller: _popover,
|
||||
offset: const Offset(0, 20),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(320, 500)),
|
||||
child: SizedBox.expand(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
|
@ -64,12 +64,9 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(320, 500)),
|
||||
child: _CellCalendarWidget(
|
||||
cellContext: widget.cellController,
|
||||
dateTypeOptionPB: _dateTypeOptionPB!,
|
||||
),
|
||||
return _CellCalendarWidget(
|
||||
cellContext: widget.cellController,
|
||||
dateTypeOptionPB: _dateTypeOptionPB!,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -302,10 +299,11 @@ class _DateTypeOptionButton extends StatelessWidget {
|
||||
return BlocSelector<DateCalBloc, DateCalState, DateTypeOptionPB>(
|
||||
selector: (state) => state.dateTypeOptionPB,
|
||||
builder: (context, dateTypeOptionPB) {
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
triggerActions:
|
||||
PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(140, 100)),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(title, fontSize: 12),
|
||||
hoverColor: theme.hover,
|
||||
@ -313,12 +311,9 @@ class _DateTypeOptionButton extends StatelessWidget {
|
||||
rightIcon: svgWidget("grid/more", color: theme.iconColor),
|
||||
),
|
||||
popupBuilder: (BuildContext popContext) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(140, 100)),
|
||||
child: _CalDateTimeSetting(
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
onEvent: (event) => context.read<DateCalBloc>().add(event),
|
||||
),
|
||||
return _CalDateTimeSetting(
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
onEvent: (event) => context.read<DateCalBloc>().add(event),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
@ -194,8 +194,10 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Popover(
|
||||
AppFlowyStylePopover(
|
||||
controller: _popover,
|
||||
constraints: BoxConstraints.loose(
|
||||
Size(SelectOptionCellEditor.editorPanelWidth, 300)),
|
||||
offset: const Offset(0, 20),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
// triggerActions: PopoverTriggerActionFlags.c,
|
||||
@ -203,18 +205,14 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
widget.onFocus?.call(true);
|
||||
});
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(
|
||||
Size(SelectOptionCellEditor.editorPanelWidth, 300)),
|
||||
child: SizedBox(
|
||||
width: SelectOptionCellEditor.editorPanelWidth,
|
||||
child: SelectOptionCellEditor(
|
||||
cellController: widget.cellControllerBuilder.build()
|
||||
as GridSelectOptionCellController,
|
||||
onDismissed: () {
|
||||
widget.onFocus?.call(false);
|
||||
},
|
||||
),
|
||||
return SizedBox(
|
||||
width: SelectOptionCellEditor.editorPanelWidth,
|
||||
child: SelectOptionCellEditor(
|
||||
cellController: widget.cellControllerBuilder.build()
|
||||
as GridSelectOptionCellController,
|
||||
onDismissed: () {
|
||||
widget.onFocus?.call(false);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -251,9 +251,10 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
controller: _popoverController,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
child: SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: Row(
|
||||
@ -286,23 +287,20 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
child: SelectOptionTypeOptionEditor(
|
||||
option: widget.option,
|
||||
onDeleted: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.deleteOption(widget.option));
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.updateOption(updatedOption));
|
||||
},
|
||||
key: ValueKey(widget.option
|
||||
.id), // Use ValueKey to refresh the UI, otherwise, it will remain the old value.
|
||||
),
|
||||
return SelectOptionTypeOptionEditor(
|
||||
option: widget.option,
|
||||
onDeleted: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.deleteOption(widget.option));
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.updateOption(updatedOption));
|
||||
},
|
||||
key: ValueKey(widget.option
|
||||
.id), // Use ValueKey to refresh the UI, otherwise, it will remain the old value.
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/url_cell_editor_bloc.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
@ -79,15 +78,12 @@ class URLEditorPopover extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: URLCellEditor(
|
||||
cellController: cellController,
|
||||
),
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: URLCellEditor(
|
||||
cellController: cellController,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -129,8 +130,9 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
),
|
||||
);
|
||||
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
controller: _popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
offset: const Offset(0, 20),
|
||||
child: SizedBox.expand(
|
||||
@ -214,7 +216,8 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
controller: _popoverController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
|
@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/type_option
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -168,7 +169,7 @@ class FieldActionCell extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
leftIcon: svgWidget(action.iconName(),
|
||||
color: enable ? theme.iconColor : theme.disableIconColor),
|
||||
color: enable ? theme.iconColor : theme.disableIconColor),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -215,9 +216,15 @@ extension _FieldActionExtension on FieldAction {
|
||||
.add(const FieldActionSheetEvent.duplicateField());
|
||||
break;
|
||||
case FieldAction.delete:
|
||||
context
|
||||
.read<FieldActionSheetBloc>()
|
||||
.add(const FieldActionSheetEvent.deleteField());
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<FieldActionSheetBloc>()
|
||||
.add(const FieldActionSheetEvent.deleteField());
|
||||
},
|
||||
).show(context);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,13 @@ 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:appflowy_popover/popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
@ -13,14 +18,16 @@ import 'field_type_option_editor.dart';
|
||||
class FieldEditor extends StatefulWidget {
|
||||
final String gridId;
|
||||
final String fieldName;
|
||||
final VoidCallback? onRemoved;
|
||||
final bool isGroupField;
|
||||
final Function(String)? onDeleted;
|
||||
|
||||
final IFieldTypeOptionLoader typeOptionLoader;
|
||||
const FieldEditor({
|
||||
required this.gridId,
|
||||
this.fieldName = "",
|
||||
required this.typeOptionLoader,
|
||||
this.onRemoved,
|
||||
this.isGroupField = false,
|
||||
this.onDeleted,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -43,10 +50,10 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
create: (context) => FieldEditorBloc(
|
||||
gridId: widget.gridId,
|
||||
fieldName: widget.fieldName,
|
||||
isGroupField: widget.isGroupField,
|
||||
loader: widget.typeOptionLoader,
|
||||
)..add(const FieldEditorEvent.initial()),
|
||||
child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
buildWhen: (p, c) => false,
|
||||
builder: (context, state) {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
@ -56,6 +63,16 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
const VSpace(10),
|
||||
const _FieldNameCell(),
|
||||
const VSpace(10),
|
||||
_DeleteFieldButton(
|
||||
popoverMutex: popoverMutex,
|
||||
onDeleted: () {
|
||||
state.field.fold(
|
||||
() => Log.error('Can not delete the field'),
|
||||
(field) => widget.onDeleted?.call(field.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(10),
|
||||
_FieldTypeOptionCell(popoverMutex: popoverMutex),
|
||||
],
|
||||
);
|
||||
@ -114,3 +131,55 @@ class _FieldNameCell extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteFieldButton extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
final VoidCallback? onDeleted;
|
||||
|
||||
const _DeleteFieldButton({
|
||||
required this.popoverMutex,
|
||||
required this.onDeleted,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, state) {
|
||||
final enable = !state.canDelete && !state.isGroupField;
|
||||
Widget button = FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_delete.tr(),
|
||||
fontSize: 12,
|
||||
color: enable ? null : theme.shader4,
|
||||
),
|
||||
);
|
||||
if (enable) button = _wrapPopover(button);
|
||||
return button;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapPopover(Widget widget) {
|
||||
return AppFlowyStylePopover(
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
constraints: BoxConstraints.loose(const Size(400, 240)),
|
||||
mutex: popoverMutex,
|
||||
direction: PopoverDirection.center,
|
||||
popupBuilder: (popupContext) {
|
||||
return PopoverAlertView(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
cancel: () => popoverMutex.state?.close(),
|
||||
confirm: () {
|
||||
onDeleted?.call();
|
||||
popoverMutex.state?.close();
|
||||
},
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
},
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -25,7 +26,7 @@ class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||
fieldType: fieldType,
|
||||
onSelectField: (fieldType) {
|
||||
onSelectField(fieldType);
|
||||
FlowyOverlay.of(context).remove(FieldTypeList.identifier());
|
||||
PopoverContainer.of(context).closeAll();
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
@ -64,19 +64,15 @@ class FieldTypeOptionEditor extends StatelessWidget {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: Popover(
|
||||
triggerActions:
|
||||
PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
|
||||
child: AppFlowyStylePopover(
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
mutex: popoverMutex,
|
||||
offset: const Offset(20, 0),
|
||||
popupBuilder: (context) {
|
||||
final list = FieldTypeList(onSelectField: (newFieldType) {
|
||||
return FieldTypeList(onSelectField: (newFieldType) {
|
||||
dataController.switchToField(newFieldType);
|
||||
});
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
child: list,
|
||||
);
|
||||
},
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(field.fieldType.title(), fontSize: 12),
|
||||
|
@ -7,7 +7,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
@ -176,9 +176,10 @@ class CreateFieldButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newColumn.tr(),
|
||||
@ -192,13 +193,10 @@ class CreateFieldButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popover) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
child: FieldEditor(
|
||||
gridId: gridId,
|
||||
fieldName: "",
|
||||
typeOptionLoader: NewFieldTypeOptionLoader(gridId: gridId),
|
||||
),
|
||||
return FieldEditor(
|
||||
gridId: gridId,
|
||||
fieldName: "",
|
||||
typeOptionLoader: NewFieldTypeOptionLoader(gridId: gridId),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -54,9 +54,10 @@ Widget? makeTypeOptionWidget({
|
||||
return builder.build(context);
|
||||
}
|
||||
|
||||
TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder(
|
||||
{required TypeOptionDataController dataController,
|
||||
required PopoverMutex popoverMutex}) {
|
||||
TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
|
||||
required TypeOptionDataController dataController,
|
||||
required PopoverMutex popoverMutex,
|
||||
}) {
|
||||
final gridId = dataController.gridId;
|
||||
final fieldType = dataController.field.fieldType;
|
||||
|
||||
|
@ -62,23 +62,21 @@ class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
}
|
||||
|
||||
Widget _renderDateFormatButton(BuildContext context, DateFormat dataFormat) {
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
mutex: popoverMutex,
|
||||
triggerActions:
|
||||
PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
popupBuilder: (popoverContext) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
child: DateFormatList(
|
||||
selectedFormat: dataFormat,
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<DateTypeOptionBloc>()
|
||||
.add(DateTypeOptionEvent.didSelectDateFormat(format));
|
||||
PopoverContainerState.of(popoverContext).closeAll();
|
||||
},
|
||||
),
|
||||
return DateFormatList(
|
||||
selectedFormat: dataFormat,
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<DateTypeOptionBloc>()
|
||||
.add(DateTypeOptionEvent.didSelectDateFormat(format));
|
||||
PopoverContainer.of(popoverContext).closeAll();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const DateFormatButton(),
|
||||
@ -86,22 +84,21 @@ class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
}
|
||||
|
||||
Widget _renderTimeFormatButton(BuildContext context, TimeFormat timeFormat) {
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
mutex: popoverMutex,
|
||||
triggerActions:
|
||||
PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
child: TimeFormatList(
|
||||
selectedFormat: timeFormat,
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<DateTypeOptionBloc>()
|
||||
.add(DateTypeOptionEvent.didSelectTimeFormat(format));
|
||||
PopoverContainerState.of(popoverContext).closeAll();
|
||||
}),
|
||||
return TimeFormatList(
|
||||
selectedFormat: timeFormat,
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<DateTypeOptionBloc>()
|
||||
.add(DateTypeOptionEvent.didSelectTimeFormat(format));
|
||||
PopoverContainer.of(popoverContext).closeAll();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: TimeFormatButton(timeFormat: timeFormat),
|
||||
|
@ -41,7 +41,7 @@ class MultiSelectTypeOptionWidget extends TypeOptionWidget {
|
||||
return SelectOptionTypeOptionWidget(
|
||||
options: selectOptionAction.typeOption.options,
|
||||
beginEdit: () {
|
||||
PopoverContainerState.of(context).closeAll();
|
||||
PopoverContainer.of(context).closeAll();
|
||||
},
|
||||
popoverMutex: popoverMutex,
|
||||
typeOptionAction: selectOptionAction,
|
||||
|
@ -55,11 +55,12 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
|
||||
listener: (context, state) =>
|
||||
typeOptionContext.typeOption = state.typeOption,
|
||||
builder: (context, state) {
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
mutex: popoverMutex,
|
||||
triggerActions: PopoverTriggerActionFlags.hover |
|
||||
PopoverTriggerActionFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
child: FlowyButton(
|
||||
margin: GridSize.typeOptionContentInsets,
|
||||
hoverColor: theme.hover,
|
||||
@ -76,17 +77,14 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
child: NumberFormatList(
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<NumberTypeOptionBloc>()
|
||||
.add(NumberTypeOptionEvent.didSelectFormat(format));
|
||||
PopoverContainerState.of(popoverContext).closeAll();
|
||||
},
|
||||
selectedFormat: state.typeOption.format,
|
||||
),
|
||||
return NumberFormatList(
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<NumberTypeOptionBloc>()
|
||||
.add(NumberTypeOptionEvent.didSelectFormat(format));
|
||||
PopoverContainer.of(popoverContext).closeAll();
|
||||
},
|
||||
selectedFormat: state.typeOption.format,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -116,6 +114,7 @@ class NumberFormatList extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _FilterTextField(),
|
||||
const VSpace(10),
|
||||
BlocBuilder<NumberFormatBloc, NumberFormatState>(
|
||||
builder: (context, state) {
|
||||
final cells = state.formats.map((format) {
|
||||
|
@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/select_opti
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -180,10 +180,11 @@ class _OptionCellState extends State<_OptionCell> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
controller: _popoverController,
|
||||
mutex: widget.popoverMutex,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
child: SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: SelectOptionTagCell(
|
||||
@ -200,24 +201,21 @@ class _OptionCellState extends State<_OptionCell> {
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
child: SelectOptionTypeOptionEditor(
|
||||
option: widget.option,
|
||||
onDeleted: () {
|
||||
context
|
||||
.read<SelectOptionTypeOptionBloc>()
|
||||
.add(SelectOptionTypeOptionEvent.deleteOption(widget.option));
|
||||
PopoverContainerState.of(popoverContext).closeAll();
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context
|
||||
.read<SelectOptionTypeOptionBloc>()
|
||||
.add(SelectOptionTypeOptionEvent.updateOption(updatedOption));
|
||||
PopoverContainerState.of(popoverContext).closeAll();
|
||||
},
|
||||
key: ValueKey(widget.option.id),
|
||||
),
|
||||
return SelectOptionTypeOptionEditor(
|
||||
option: widget.option,
|
||||
onDeleted: () {
|
||||
context
|
||||
.read<SelectOptionTypeOptionBloc>()
|
||||
.add(SelectOptionTypeOptionEvent.deleteOption(widget.option));
|
||||
PopoverContainer.of(popoverContext).closeAll();
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context
|
||||
.read<SelectOptionTypeOptionBloc>()
|
||||
.add(SelectOptionTypeOptionEvent.updateOption(updatedOption));
|
||||
PopoverContainer.of(popoverContext).closeAll();
|
||||
},
|
||||
key: ValueKey(widget.option.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -40,7 +40,7 @@ class SingleSelectTypeOptionWidget extends TypeOptionWidget {
|
||||
return SelectOptionTypeOptionWidget(
|
||||
options: selectOptionAction.typeOption.options,
|
||||
beginEdit: () {
|
||||
PopoverContainerState.of(context).closeAll();
|
||||
PopoverContainer.of(context).closeAll();
|
||||
},
|
||||
popoverMutex: popoverMutex,
|
||||
typeOptionAction: selectOptionAction,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_action_sheet_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
@ -150,9 +151,15 @@ extension _RowActionExtension on _RowAction {
|
||||
.add(const RowActionSheetEvent.duplicateRow());
|
||||
break;
|
||||
case _RowAction.delete:
|
||||
context
|
||||
.read<RowActionSheetBloc>()
|
||||
.add(const RowActionSheetEvent.deleteRow());
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<RowActionSheetBloc>()
|
||||
.add(const RowActionSheetEvent.deleteRow());
|
||||
},
|
||||
).show(context);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +133,7 @@ class _PropertyList extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(10),
|
||||
_CreateFieldButton(
|
||||
viewId: viewId,
|
||||
onClosed: () {
|
||||
@ -144,13 +145,16 @@ class _PropertyList extends StatelessWidget {
|
||||
);
|
||||
});
|
||||
},
|
||||
onOpened: () {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
child: FieldEditor(
|
||||
gridId: viewId,
|
||||
typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
|
||||
),
|
||||
onOpened: (controller) {
|
||||
return FieldEditor(
|
||||
gridId: viewId,
|
||||
typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
|
||||
onDeleted: (fieldId) {
|
||||
controller.close();
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -161,10 +165,11 @@ class _PropertyList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateFieldButton extends StatelessWidget {
|
||||
class _CreateFieldButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
final Widget Function() onOpened;
|
||||
final Widget Function(PopoverController) onOpened;
|
||||
final VoidCallback onClosed;
|
||||
|
||||
const _CreateFieldButton({
|
||||
required this.viewId,
|
||||
required this.onOpened,
|
||||
@ -172,16 +177,32 @@ class _CreateFieldButton extends StatelessWidget {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_CreateFieldButton> createState() => _CreateFieldButtonState();
|
||||
}
|
||||
|
||||
class _CreateFieldButtonState extends State<_CreateFieldButton> {
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
controller: popoverController,
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
onClose: onClosed,
|
||||
child: SizedBox(
|
||||
direction: PopoverDirection.topWithLeftAligned,
|
||||
onClose: widget.onClosed,
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newColumn.tr(),
|
||||
@ -192,7 +213,17 @@ class _CreateFieldButton extends StatelessWidget {
|
||||
leftIcon: svgWidget("home/add"),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext context) => onOpened(),
|
||||
popupBuilder: (BuildContext context) =>
|
||||
widget.onOpened(popoverController),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
final borderSide = BorderSide(color: theme.shader6, width: 1.0);
|
||||
return BoxDecoration(
|
||||
color: theme.surface,
|
||||
border: Border(top: borderSide),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -241,16 +272,23 @@ class _RowDetailCellState extends State<_RowDetailCell> {
|
||||
child: Popover(
|
||||
controller: popover,
|
||||
offset: const Offset(20, 0),
|
||||
popupBuilder: (context) {
|
||||
popupBuilder: (popoverContext) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
child: FieldEditor(
|
||||
gridId: widget.cellId.gridId,
|
||||
fieldName: widget.cellId.fieldContext.field.name,
|
||||
isGroupField: widget.cellId.fieldContext.isGroupField,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
gridId: widget.cellId.gridId,
|
||||
field: widget.cellId.fieldContext.field,
|
||||
),
|
||||
onDeleted: (fieldId) {
|
||||
popover.close();
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -3,7 +3,6 @@ import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -15,9 +14,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
class GridGroupList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final GridFieldController fieldController;
|
||||
final VoidCallback onDismissed;
|
||||
const GridGroupList({
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
required this.onDismissed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -33,6 +34,7 @@ class GridGroupList extends StatelessWidget {
|
||||
final cells = state.fieldContexts.map((fieldContext) {
|
||||
Widget cell = _GridGroupCell(
|
||||
fieldContext: fieldContext,
|
||||
onSelected: () => onDismissed(),
|
||||
key: ValueKey(fieldContext.id),
|
||||
);
|
||||
|
||||
@ -56,29 +58,16 @@ class GridGroupList extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void show(BuildContext context) {
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
child: this,
|
||||
),
|
||||
identifier: identifier(),
|
||||
anchorContext: context,
|
||||
anchorDirection: AnchorDirection.bottomRight,
|
||||
style: FlowyOverlayStyle(blur: false),
|
||||
);
|
||||
}
|
||||
|
||||
static String identifier() {
|
||||
return (GridGroupList).toString();
|
||||
}
|
||||
}
|
||||
|
||||
class _GridGroupCell extends StatelessWidget {
|
||||
final VoidCallback onSelected;
|
||||
final GridFieldContext fieldContext;
|
||||
const _GridGroupCell({required this.fieldContext, Key? key})
|
||||
: super(key: key);
|
||||
const _GridGroupCell({
|
||||
required this.fieldContext,
|
||||
required this.onSelected,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -97,8 +86,10 @@ class _GridGroupCell extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(fieldContext.name, fontSize: 12),
|
||||
hoverColor: theme.hover,
|
||||
leftIcon: svgWidget(fieldContext.fieldType.iconName(),
|
||||
color: theme.iconColor),
|
||||
leftIcon: svgWidget(
|
||||
fieldContext.fieldType.iconName(),
|
||||
color: theme.iconColor,
|
||||
),
|
||||
rightIcon: rightIcon,
|
||||
onTap: () {
|
||||
context.read<GridGroupBloc>().add(
|
||||
@ -107,7 +98,8 @@ class _GridGroupCell extends StatelessWidget {
|
||||
fieldContext.fieldType,
|
||||
),
|
||||
);
|
||||
FlowyOverlay.of(context).remove(GridGroupList.identifier());
|
||||
onSelected();
|
||||
// FlowyOverlay.of(context).remove(GridGroupList.identifier());
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -116,10 +116,11 @@ class _GridPropertyCell extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _editFieldButton(AppTheme theme, BuildContext context) {
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
mutex: popoverMutex,
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(fieldContext.name, fontSize: 12),
|
||||
hoverColor: theme.hover,
|
||||
@ -127,14 +128,11 @@ class _GridPropertyCell extends StatelessWidget {
|
||||
color: theme.iconColor),
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
child: FieldEditor(
|
||||
gridId: gridId,
|
||||
fieldName: fieldContext.name,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
gridId: gridId, field: fieldContext.field),
|
||||
),
|
||||
return FieldEditor(
|
||||
gridId: gridId,
|
||||
fieldName: fieldContext.name,
|
||||
typeOptionLoader:
|
||||
FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -53,7 +53,8 @@ class _SettingButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Popover(
|
||||
return AppFlowyStylePopover(
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
triggerActions: PopoverTriggerActionFlags.click,
|
||||
offset: const Offset(0, 10),
|
||||
child: FlowyIconButton(
|
||||
@ -87,25 +88,19 @@ class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_action == GridSettingAction.properties) {
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
child: GridPropertyList(
|
||||
gridId: widget.settingContext.gridId,
|
||||
fieldController: widget.settingContext.fieldController,
|
||||
),
|
||||
return GridPropertyList(
|
||||
gridId: widget.settingContext.gridId,
|
||||
fieldController: widget.settingContext.fieldController,
|
||||
);
|
||||
}
|
||||
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(140, 400)),
|
||||
child: GridSettingList(
|
||||
settingContext: widget.settingContext,
|
||||
onAction: (action, settingContext) {
|
||||
setState(() {
|
||||
_action = action;
|
||||
});
|
||||
},
|
||||
),
|
||||
return GridSettingList(
|
||||
settingContext: widget.settingContext,
|
||||
onAction: (action, settingContext) {
|
||||
setState(() {
|
||||
_action = action;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class NewAppButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _showCreateAppDialog(BuildContext context) async {
|
||||
return TextFieldDialog(
|
||||
return NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.newPageText.tr(),
|
||||
value: "",
|
||||
confirm: (newValue) {
|
||||
|
@ -126,7 +126,7 @@ class MenuAppHeader extends StatelessWidget {
|
||||
action.fold(() {}, (action) {
|
||||
switch (action) {
|
||||
case AppDisclosureAction.rename:
|
||||
TextFieldDialog(
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
|
||||
value: context.read<AppBloc>().state.app.name,
|
||||
confirm: (newValue) {
|
||||
|
@ -109,7 +109,7 @@ class ViewSectionItem extends StatelessWidget {
|
||||
action.foldRight({}, (action, previous) {
|
||||
switch (action) {
|
||||
case ViewDisclosureAction.rename:
|
||||
TextFieldDialog(
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.disclosureAction_rename.tr(),
|
||||
value: context.read<ViewBloc>().state.view.name,
|
||||
confirm: (newValue) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
@ -15,13 +16,13 @@ import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
|
||||
class TextFieldDialog extends StatefulWidget {
|
||||
class NavigatorTextFieldDialog extends StatefulWidget {
|
||||
final String value;
|
||||
final String title;
|
||||
final void Function()? cancel;
|
||||
final void Function(String) confirm;
|
||||
|
||||
const TextFieldDialog({
|
||||
const NavigatorTextFieldDialog({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.confirm,
|
||||
@ -30,10 +31,10 @@ class TextFieldDialog extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TextFieldDialog> createState() => _CreateTextFieldDialog();
|
||||
State<NavigatorTextFieldDialog> createState() => _CreateTextFieldDialog();
|
||||
}
|
||||
|
||||
class _CreateTextFieldDialog extends State<TextFieldDialog> {
|
||||
class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
|
||||
String newValue = "";
|
||||
|
||||
@override
|
||||
@ -56,7 +57,8 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
|
||||
FlowyFormTextInput(
|
||||
hintText: LocaleKeys.dialogCreatePageNameHint.tr(),
|
||||
initialValue: widget.value,
|
||||
textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
|
||||
textStyle:
|
||||
const TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
|
||||
autoFocus: true,
|
||||
onChanged: (text) {
|
||||
newValue = text;
|
||||
@ -70,11 +72,13 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
|
||||
OkCancelButton(
|
||||
onOkPressed: () {
|
||||
widget.confirm(newValue);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onCancelPressed: () {
|
||||
if (widget.cancel != null) {
|
||||
widget.cancel!();
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -83,12 +87,14 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
class FlowyAlertDialog extends StatefulWidget {
|
||||
class PopoverAlertView extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
final String title;
|
||||
final void Function()? cancel;
|
||||
final void Function()? confirm;
|
||||
|
||||
const FlowyAlertDialog({
|
||||
const PopoverAlertView({
|
||||
required this.popoverMutex,
|
||||
required this.title,
|
||||
this.confirm,
|
||||
this.cancel,
|
||||
@ -96,10 +102,46 @@ class FlowyAlertDialog extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<FlowyAlertDialog> createState() => _CreateFlowyAlertDialog();
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return StyledDialog(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
...[
|
||||
FlowyText.medium(title, color: theme.shader4),
|
||||
],
|
||||
if (confirm != null) ...[
|
||||
const VSpace(20),
|
||||
OkCancelButton(
|
||||
onOkPressed: confirm,
|
||||
onCancelPressed: cancel,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
|
||||
class NavigatorAlertDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final void Function()? cancel;
|
||||
final void Function()? confirm;
|
||||
|
||||
const NavigatorAlertDialog({
|
||||
required this.title,
|
||||
this.confirm,
|
||||
this.cancel,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<NavigatorAlertDialog> createState() => _CreateFlowyAlertDialog();
|
||||
}
|
||||
|
||||
class _CreateFlowyAlertDialog extends State<NavigatorAlertDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -118,10 +160,13 @@ class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
|
||||
],
|
||||
if (widget.confirm != null) ...[
|
||||
const VSpace(20),
|
||||
OkCancelButton(
|
||||
onOkPressed: widget.confirm!,
|
||||
onCancelPressed: widget.confirm,
|
||||
)
|
||||
OkCancelButton(onOkPressed: () {
|
||||
widget.confirm?.call();
|
||||
Navigator.of(context).pop();
|
||||
}, onCancelPressed: () {
|
||||
widget.cancel?.call();
|
||||
Navigator.of(context).pop();
|
||||
})
|
||||
]
|
||||
],
|
||||
),
|
||||
@ -129,7 +174,7 @@ class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
class OkCancelDialog extends StatelessWidget {
|
||||
class NavigatorOkCancelDialog extends StatelessWidget {
|
||||
final VoidCallback? onOkPressed;
|
||||
final VoidCallback? onCancelPressed;
|
||||
final String? okTitle;
|
||||
@ -138,7 +183,7 @@ class OkCancelDialog extends StatelessWidget {
|
||||
final String message;
|
||||
final double? maxWidth;
|
||||
|
||||
const OkCancelDialog(
|
||||
const NavigatorOkCancelDialog(
|
||||
{Key? key,
|
||||
this.onOkPressed,
|
||||
this.onCancelPressed,
|
||||
@ -158,7 +203,7 @@ class OkCancelDialog extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (title != null) ...[
|
||||
Text(title!.toUpperCase(), style: TextStyles.T1.textColor(theme.shader1)),
|
||||
FlowyText.medium(title!.toUpperCase(), color: theme.shader1),
|
||||
VSpace(Insets.sm * 1.5),
|
||||
Container(color: theme.bg1, height: 1),
|
||||
VSpace(Insets.m * 1.5),
|
||||
@ -166,8 +211,14 @@ class OkCancelDialog extends StatelessWidget {
|
||||
Text(message, style: TextStyles.Body1.textHeight(1.5)),
|
||||
SizedBox(height: Insets.l),
|
||||
OkCancelButton(
|
||||
onOkPressed: onOkPressed,
|
||||
onCancelPressed: onCancelPressed,
|
||||
onOkPressed: () {
|
||||
onOkPressed?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onCancelPressed: () {
|
||||
onCancelPressed?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
okTitle: okTitle?.toUpperCase(),
|
||||
cancelTitle: cancelTitle?.toUpperCase(),
|
||||
)
|
||||
@ -185,7 +236,12 @@ class OkCancelButton extends StatelessWidget {
|
||||
final double? minHeight;
|
||||
|
||||
const OkCancelButton(
|
||||
{Key? key, this.onOkPressed, this.onCancelPressed, this.okTitle, this.cancelTitle, this.minHeight})
|
||||
{Key? key,
|
||||
this.onOkPressed,
|
||||
this.onCancelPressed,
|
||||
this.okTitle,
|
||||
this.cancelTitle,
|
||||
this.minHeight})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@ -198,20 +254,14 @@ class OkCancelButton extends StatelessWidget {
|
||||
if (onCancelPressed != null)
|
||||
SecondaryTextButton(
|
||||
cancelTitle ?? LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () {
|
||||
onCancelPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
},
|
||||
onPressed: onCancelPressed,
|
||||
bigMode: true,
|
||||
),
|
||||
HSpace(Insets.m),
|
||||
if (onOkPressed != null)
|
||||
PrimaryTextButton(
|
||||
okTitle ?? LocaleKeys.button_OK.tr(),
|
||||
onPressed: () {
|
||||
onOkPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
},
|
||||
onPressed: onOkPressed,
|
||||
bigMode: true,
|
||||
),
|
||||
],
|
||||
|
@ -416,6 +416,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = arm64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
@ -549,6 +550,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = arm64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
@ -573,6 +575,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = arm64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
|
@ -58,122 +58,153 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
||||
key: 'Move cursor top',
|
||||
command: 'meta+arrow up',
|
||||
windowsCommand: 'ctrl+arrow up',
|
||||
linuxCommand: 'ctrl+arrow up',
|
||||
handler: cursorTop,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Move cursor bottom',
|
||||
command: 'meta+arrow down',
|
||||
windowsCommand: 'ctrl+arrow down',
|
||||
linuxCommand: 'ctrl+arrow down',
|
||||
handler: cursorBottom,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Move cursor begin',
|
||||
command: 'meta+arrow left',
|
||||
windowsCommand: 'ctrl+arrow left',
|
||||
linuxCommand: 'ctrl+arrow left',
|
||||
handler: cursorBegin,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Move cursor end',
|
||||
command: 'meta+arrow right',
|
||||
windowsCommand: 'ctrl+arrow right',
|
||||
linuxCommand: 'ctrl+arrow right',
|
||||
handler: cursorEnd,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Cursor top select',
|
||||
command: 'meta+shift+arrow up',
|
||||
windowsCommand: 'ctrl+shift+arrow up',
|
||||
linuxCommand: 'ctrl+shift+arrow up',
|
||||
handler: cursorTopSelect,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Cursor bottom select',
|
||||
command: 'meta+shift+arrow down',
|
||||
windowsCommand: 'ctrl+shift+arrow down',
|
||||
linuxCommand: 'ctrl+shift+arrow down',
|
||||
handler: cursorBottomSelect,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Cursor begin select',
|
||||
command: 'meta+shift+arrow left',
|
||||
windowsCommand: 'ctrl+shift+arrow left',
|
||||
linuxCommand: 'ctrl+shift+arrow left',
|
||||
handler: cursorBeginSelect,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Cursor end select',
|
||||
command: 'meta+shift+arrow right',
|
||||
windowsCommand: 'ctrl+shift+arrow right',
|
||||
linuxCommand: 'ctrl+shift+arrow right',
|
||||
handler: cursorEndSelect,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Redo',
|
||||
command: 'meta+shift+z',
|
||||
windowsCommand: 'ctrl+shift+z',
|
||||
linuxCommand: 'ctrl+shift+z',
|
||||
handler: redoEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Undo',
|
||||
command: 'meta+z',
|
||||
windowsCommand: 'ctrl+z',
|
||||
linuxCommand: 'ctrl+z',
|
||||
handler: undoEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Format bold',
|
||||
command: 'meta+b',
|
||||
windowsCommand: 'ctrl+b',
|
||||
linuxCommand: 'ctrl+b',
|
||||
handler: formatBoldEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Format italic',
|
||||
command: 'meta+i',
|
||||
windowsCommand: 'ctrl+i',
|
||||
linuxCommand: 'ctrl+i',
|
||||
handler: formatItalicEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Format underline',
|
||||
command: 'meta+u',
|
||||
windowsCommand: 'ctrl+u',
|
||||
linuxCommand: 'ctrl+u',
|
||||
handler: formatUnderlineEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Format strikethrough',
|
||||
command: 'meta+shift+s',
|
||||
windowsCommand: 'ctrl+shift+s',
|
||||
linuxCommand: 'ctrl+shift+s',
|
||||
handler: formatStrikethroughEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Format highlight',
|
||||
command: 'meta+shift+h',
|
||||
windowsCommand: 'ctrl+shift+h',
|
||||
linuxCommand: 'ctrl+shift+h',
|
||||
handler: formatHighlightEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Format embed code',
|
||||
command: 'meta+e',
|
||||
windowsCommand: 'ctrl+e',
|
||||
linuxCommand: 'ctrl+e',
|
||||
handler: formatEmbedCodeEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Format link',
|
||||
command: 'meta+k',
|
||||
windowsCommand: 'ctrl+k',
|
||||
linuxCommand: 'ctrl+k',
|
||||
handler: formatLinkEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Copy',
|
||||
command: 'meta+c',
|
||||
windowsCommand: 'ctrl+c',
|
||||
linuxCommand: 'ctrl+c',
|
||||
handler: copyEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Paste',
|
||||
command: 'meta+v',
|
||||
windowsCommand: 'ctrl+v',
|
||||
linuxCommand: 'ctrl+v',
|
||||
handler: pasteEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Paste',
|
||||
key: 'Cut',
|
||||
command: 'meta+x',
|
||||
windowsCommand: 'ctrl+x',
|
||||
linuxCommand: 'ctrl+x',
|
||||
handler: cutEventHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Home',
|
||||
command: 'home',
|
||||
handler: cursorBegin,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'End',
|
||||
command: 'end',
|
||||
handler: cursorEnd,
|
||||
),
|
||||
|
||||
// TODO: split the keys.
|
||||
ShortcutEvent(
|
||||
key: 'Delete Text',
|
||||
@ -199,6 +230,7 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
||||
key: 'select all',
|
||||
command: 'meta+a',
|
||||
windowsCommand: 'ctrl+a',
|
||||
linuxCommand: 'ctrl+a',
|
||||
handler: selectAllHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
|
@ -287,7 +287,7 @@ void main() async {
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
@ -321,7 +321,7 @@ void main() async {
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
@ -398,7 +398,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
|
||||
}
|
||||
}
|
||||
await editor.updateSelection(selection);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isControlPressed: true,
|
||||
@ -415,7 +415,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
|
||||
Selection.single(path: [0], startOffset: 0),
|
||||
);
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isControlPressed: true,
|
||||
@ -432,7 +432,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
|
||||
Selection.single(path: [0], startOffset: text.length),
|
||||
);
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
isControlPressed: true,
|
||||
@ -449,7 +449,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
|
||||
Selection.single(path: [0], startOffset: 0),
|
||||
);
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
isControlPressed: true,
|
||||
|
@ -94,7 +94,7 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
var selection =
|
||||
Selection.single(path: [1], startOffset: 2, endOffset: text.length - 2);
|
||||
await editor.updateSelection(selection);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
key,
|
||||
isShiftPressed: isShiftPressed,
|
||||
@ -121,7 +121,7 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
selection =
|
||||
Selection.single(path: [1], startOffset: 0, endOffset: text.length);
|
||||
await editor.updateSelection(selection);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
key,
|
||||
isShiftPressed: isShiftPressed,
|
||||
@ -146,7 +146,7 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
true);
|
||||
|
||||
await editor.updateSelection(selection);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
key,
|
||||
isShiftPressed: isShiftPressed,
|
||||
@ -168,7 +168,7 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
end: Position(path: [2], offset: text.length),
|
||||
);
|
||||
await editor.updateSelection(selection);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
key,
|
||||
isShiftPressed: isShiftPressed,
|
||||
@ -203,7 +203,7 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
|
||||
await editor.updateSelection(selection);
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
key,
|
||||
isShiftPressed: isShiftPressed,
|
||||
@ -249,7 +249,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
|
||||
expect(find.byType(ToolbarWidget), findsOneWidget);
|
||||
|
||||
// trigger the link menu
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true);
|
||||
} else {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true);
|
||||
@ -272,7 +272,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
|
||||
true);
|
||||
|
||||
await editor.updateSelection(selection);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true);
|
||||
} else {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true);
|
||||
@ -289,7 +289,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
|
||||
expect(find.byType(LinkMenu), findsNothing);
|
||||
|
||||
// Remove link
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true);
|
||||
} else {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true);
|
||||
|
@ -43,7 +43,7 @@ Future<void> _testBackspaceUndoRedo(
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(editor.documentLength, 2);
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: true,
|
||||
@ -59,7 +59,7 @@ Future<void> _testBackspaceUndoRedo(
|
||||
expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text);
|
||||
expect(editor.documentSelection, selection);
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: true,
|
||||
|
@ -28,7 +28,7 @@ Future<void> _testSelectAllHandler(WidgetTester tester, int lines) async {
|
||||
editor.insertTextNode(text);
|
||||
}
|
||||
await editor.startTesting();
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyA, isControlPressed: true);
|
||||
} else {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);
|
||||
|
@ -39,7 +39,7 @@ void main() async {
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isControlPressed: true,
|
||||
@ -62,11 +62,12 @@ void main() async {
|
||||
if (event.key == 'Move cursor begin') {
|
||||
event.updateCommand(
|
||||
windowsCommand: 'alt+arrow left',
|
||||
linuxCommand: 'alt+arrow left',
|
||||
macOSCommand: 'alt+arrow left',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (Platform.isWindows || Platform.isMacOS) {
|
||||
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isAltPressed: true,
|
||||
@ -84,5 +85,139 @@ void main() async {
|
||||
|
||||
tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('redefine move cursor end command', (tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0),
|
||||
);
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isControlPressed: true,
|
||||
);
|
||||
} else {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isMetaPressed: true,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0),
|
||||
);
|
||||
|
||||
for (final event in builtInShortcutEvents) {
|
||||
if (event.key == 'Move cursor end') {
|
||||
event.updateCommand(
|
||||
windowsCommand: 'alt+arrow right',
|
||||
linuxCommand: 'alt+arrow right',
|
||||
macOSCommand: 'alt+arrow right',
|
||||
);
|
||||
}
|
||||
}
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isAltPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Test Home Key to move to start of current text',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.home,
|
||||
);
|
||||
}
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: 0),
|
||||
);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
|
||||
for (final event in builtInShortcutEvents) {
|
||||
if (event.key == 'Move cursor begin') {
|
||||
event.updateCommand(
|
||||
windowsCommand: 'home',
|
||||
linuxCommand: 'home',
|
||||
macOSCommand: 'home',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.home,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: 0),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Test End Key to move to end of current text', (tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.end,
|
||||
);
|
||||
}
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0),
|
||||
);
|
||||
|
||||
for (final event in builtInShortcutEvents) {
|
||||
if (event.key == 'Move cursor end') {
|
||||
event.updateCommand(
|
||||
windowsCommand: 'end',
|
||||
linuxCommand: 'end',
|
||||
macOSCommand: 'end',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.end,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class Popover extends StatefulWidget {
|
||||
final Decoration? maskDecoration;
|
||||
|
||||
/// The function used to build the popover.
|
||||
final Widget Function(BuildContext context) popupBuilder;
|
||||
final Widget? Function(BuildContext context) popupBuilder;
|
||||
|
||||
final int triggerActions;
|
||||
|
||||
@ -265,7 +265,7 @@ class _PopoverMaskState extends State<_PopoverMask> {
|
||||
}
|
||||
|
||||
class PopoverContainer extends StatefulWidget {
|
||||
final Widget Function(BuildContext context) popupBuilder;
|
||||
final Widget? Function(BuildContext context) popupBuilder;
|
||||
final PopoverDirection direction;
|
||||
final PopoverLink popoverLink;
|
||||
final Offset offset;
|
||||
@ -284,6 +284,15 @@ class PopoverContainer extends StatefulWidget {
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => PopoverContainerState();
|
||||
|
||||
static PopoverContainerState of(BuildContext context) {
|
||||
if (context is StatefulElement && context.state is PopoverContainerState) {
|
||||
return context.state as PopoverContainerState;
|
||||
}
|
||||
final PopoverContainerState? result =
|
||||
context.findAncestorStateOfType<PopoverContainerState>();
|
||||
return result!;
|
||||
}
|
||||
}
|
||||
|
||||
class PopoverContainerState extends State<PopoverContainer> {
|
||||
@ -302,13 +311,4 @@ class PopoverContainerState extends State<PopoverContainer> {
|
||||
close() => widget.onClose();
|
||||
|
||||
closeAll() => widget.onCloseAll();
|
||||
|
||||
static PopoverContainerState of(BuildContext context) {
|
||||
if (context is StatefulElement && context.state is PopoverContainerState) {
|
||||
return context.state as PopoverContainerState;
|
||||
}
|
||||
final PopoverContainerState? result =
|
||||
context.findAncestorStateOfType<PopoverContainerState>();
|
||||
return result!;
|
||||
}
|
||||
}
|
||||
|
@ -9,4 +9,4 @@ export 'src/flowy_overlay/flowy_overlay.dart';
|
||||
export 'src/flowy_overlay/list_overlay.dart';
|
||||
export 'src/flowy_overlay/option_overlay.dart';
|
||||
export 'src/flowy_overlay/flowy_dialog.dart';
|
||||
export 'src/flowy_overlay/flowy_popover.dart';
|
||||
export 'src/flowy_overlay/appflowy_stype_popover.dart';
|
||||
|
@ -0,0 +1,48 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:appflowy_popover/popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppFlowyStylePopover extends StatelessWidget {
|
||||
final Widget child;
|
||||
final PopoverController? controller;
|
||||
final Widget Function(BuildContext context) popupBuilder;
|
||||
final PopoverDirection direction;
|
||||
final int triggerActions;
|
||||
final BoxConstraints? constraints;
|
||||
final void Function()? onClose;
|
||||
final PopoverMutex? mutex;
|
||||
final Offset? offset;
|
||||
|
||||
const AppFlowyStylePopover({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.popupBuilder,
|
||||
this.direction = PopoverDirection.rightWithTopAligned,
|
||||
this.onClose,
|
||||
this.constraints,
|
||||
this.mutex,
|
||||
this.triggerActions = 0,
|
||||
this.offset,
|
||||
this.controller,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Popover(
|
||||
controller: controller,
|
||||
onClose: onClose,
|
||||
direction: direction,
|
||||
mutex: mutex,
|
||||
triggerActions: triggerActions,
|
||||
popupBuilder: (context) {
|
||||
final child = popupBuilder(context);
|
||||
debugPrint('$child popover');
|
||||
return OverlayContainer(
|
||||
constraints: constraints,
|
||||
child: popupBuilder(context),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import './flowy_popover_layout.dart';
|
||||
|
||||
const _overlayContainerPadding = EdgeInsets.all(12);
|
||||
|
||||
class FlowyPopover extends StatefulWidget {
|
||||
final Widget Function(BuildContext context) builder;
|
||||
final ShapeBorder? shape;
|
||||
final Rect anchorRect;
|
||||
final AnchorDirection? anchorDirection;
|
||||
final EdgeInsets padding;
|
||||
final BoxConstraints? constraints;
|
||||
|
||||
const FlowyPopover({
|
||||
Key? key,
|
||||
required this.builder,
|
||||
required this.anchorRect,
|
||||
this.shape,
|
||||
this.padding = _overlayContainerPadding,
|
||||
this.anchorDirection,
|
||||
this.constraints,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<FlowyPopover> createState() => _FlowyPopoverState();
|
||||
}
|
||||
|
||||
class _FlowyPopoverState extends State<FlowyPopover> {
|
||||
final preRenderKey = GlobalKey();
|
||||
Size? size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme =
|
||||
context.watch<AppTheme?>() ?? AppTheme.fromType(ThemeType.light);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: PopoverLayoutDelegate(
|
||||
anchorRect: widget.anchorRect,
|
||||
anchorDirection:
|
||||
widget.anchorDirection ?? AnchorDirection.rightWithTopAligned,
|
||||
overlapBehaviour: OverlapBehaviour.stretch,
|
||||
),
|
||||
child: Container(
|
||||
padding: widget.padding,
|
||||
constraints: widget.constraints ??
|
||||
BoxConstraints.loose(const Size(280, 400)),
|
||||
decoration: FlowyDecoration.decoration(
|
||||
theme.surface, theme.shadowColor.withOpacity(0.15)),
|
||||
key: preRenderKey,
|
||||
child: widget.builder(context),
|
||||
)));
|
||||
}
|
||||
}
|
@ -1,22 +1,30 @@
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'base_styled_button.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
|
||||
class PrimaryTextButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const PrimaryTextButton(this.label, {Key? key, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const PrimaryTextButton(this.label,
|
||||
{Key? key, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle txtStyle = TextStyles.Btn.textColor(Colors.white);
|
||||
return PrimaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
|
||||
final theme = context.watch<AppTheme>();
|
||||
return PrimaryButton(
|
||||
bigMode: bigMode,
|
||||
onPressed: onPressed,
|
||||
child: FlowyText.regular(
|
||||
label,
|
||||
color: theme.surface,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,14 +33,16 @@ class PrimaryButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const PrimaryButton({Key? key, required this.child, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const PrimaryButton(
|
||||
{Key? key, required this.child, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BaseStyledButton(
|
||||
minWidth: bigMode ? 170 : 78,
|
||||
minHeight: bigMode ? 48 : 28,
|
||||
minWidth: bigMode ? 100 : 80,
|
||||
minHeight: bigMode ? 40 : 38,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: theme.main1,
|
||||
hoverColor: theme.main1,
|
||||
|
@ -1,9 +1,8 @@
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'base_styled_button.dart';
|
||||
|
||||
@ -12,13 +11,21 @@ class SecondaryTextButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const SecondaryTextButton(this.label, {Key? key, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const SecondaryTextButton(this.label,
|
||||
{Key? key, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
TextStyle txtStyle = TextStyles.Btn.textColor(theme.main1);
|
||||
return SecondaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
|
||||
return SecondaryButton(
|
||||
bigMode: bigMode,
|
||||
onPressed: onPressed,
|
||||
child: FlowyText.regular(
|
||||
label,
|
||||
color: theme.main1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,14 +34,16 @@ class SecondaryButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const SecondaryButton({Key? key, required this.child, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const SecondaryButton(
|
||||
{Key? key, required this.child, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BaseStyledButton(
|
||||
minWidth: bigMode ? 170 : 78,
|
||||
minHeight: bigMode ? 48 : 28,
|
||||
minWidth: bigMode ? 100 : 80,
|
||||
minHeight: bigMode ? 40 : 38,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: theme.shader7,
|
||||
hoverColor: theme.hover,
|
||||
|
@ -1,3 +1,3 @@
|
||||
class DialogSize {
|
||||
static double get minDialogWidth => 480;
|
||||
static double get minDialogWidth => 400;
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class StyledDialog extends StatelessWidget {
|
||||
|
||||
return FocusTraversalGroup(
|
||||
child: Container(
|
||||
margin: margin ?? EdgeInsets.all(Insets.lGutter * 2),
|
||||
margin: margin ?? EdgeInsets.all(Insets.sm * 2),
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
@ -133,7 +133,9 @@ class StyledDialogRoute<T> extends PopupRoute<T> {
|
||||
super(settings: settings, filter: barrier.filter);
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => barrier.dismissible;
|
||||
bool get barrierDismissible {
|
||||
return barrier.dismissible;
|
||||
}
|
||||
|
||||
@override
|
||||
String get barrierLabel => barrier.label;
|
||||
|
@ -27,6 +27,8 @@ dependencies:
|
||||
path: flowy_infra_ui_platform_interface
|
||||
flowy_infra_ui_web:
|
||||
path: flowy_infra_ui_web
|
||||
appflowy_popover:
|
||||
path: ../appflowy_popover
|
||||
|
||||
# Flowy packages
|
||||
flowy_infra:
|
||||
|
@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB};
|
||||
use flowy_text_block::BlockCloudService;
|
||||
use flowy_text_block::TextEditorCloudService;
|
||||
use http_flowy::response::FlowyResponse;
|
||||
use lazy_static::lazy_static;
|
||||
use lib_infra::future::FutureResult;
|
||||
@ -20,20 +20,20 @@ impl BlockHttpCloudService {
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockCloudService for BlockHttpCloudService {
|
||||
fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
impl TextEditorCloudService for BlockHttpCloudService {
|
||||
fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
let token = token.to_owned();
|
||||
let url = self.config.doc_url();
|
||||
FutureResult::new(async move { create_document_request(&token, params, &url).await })
|
||||
}
|
||||
|
||||
fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
|
||||
fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
|
||||
let token = token.to_owned();
|
||||
let url = self.config.doc_url();
|
||||
FutureResult::new(async move { read_document_request(&token, params, &url).await })
|
||||
}
|
||||
|
||||
fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
let token = token.to_owned();
|
||||
let url = self.config.doc_url();
|
||||
FutureResult::new(async move { reset_doc_request(&token, params, &url).await })
|
||||
|
@ -261,7 +261,7 @@ use flowy_folder::entities::{
|
||||
use flowy_folder_data_model::revision::{
|
||||
gen_app_id, gen_workspace_id, AppRevision, TrashRevision, ViewRevision, WorkspaceRevision,
|
||||
};
|
||||
use flowy_text_block::BlockCloudService;
|
||||
use flowy_text_block::TextEditorCloudService;
|
||||
use flowy_user::entities::{
|
||||
SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB,
|
||||
};
|
||||
@ -414,12 +414,12 @@ impl UserCloudService for LocalServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockCloudService for LocalServer {
|
||||
fn create_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
impl TextEditorCloudService for LocalServer {
|
||||
fn create_text_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
FutureResult::new(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn read_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
|
||||
fn read_text_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
|
||||
let doc = DocumentPB {
|
||||
block_id: params.value,
|
||||
text: initial_quill_delta_string(),
|
||||
@ -429,7 +429,7 @@ impl BlockCloudService for LocalServer {
|
||||
FutureResult::new(async { Ok(Some(doc)) })
|
||||
}
|
||||
|
||||
fn update_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
fn update_text_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
|
||||
FutureResult::new(async { Ok(()) })
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver};
|
||||
use flowy_sync::client_document::default::initial_quill_delta_string;
|
||||
use flowy_sync::entities::revision::{RepeatedRevision, Revision};
|
||||
use flowy_sync::entities::ws_data::ClientRevisionWSData;
|
||||
use flowy_text_block::TextBlockManager;
|
||||
use flowy_text_block::TextEditorManager;
|
||||
use flowy_user::services::UserSession;
|
||||
use futures_core::future::BoxFuture;
|
||||
use lib_infra::future::{BoxResultFuture, FutureResult};
|
||||
@ -34,7 +34,7 @@ impl FolderDepsResolver {
|
||||
user_session: Arc<UserSession>,
|
||||
server_config: &ClientServerConfiguration,
|
||||
ws_conn: &Arc<FlowyWebSocketConnect>,
|
||||
text_block_manager: &Arc<TextBlockManager>,
|
||||
text_block_manager: &Arc<TextEditorManager>,
|
||||
grid_manager: &Arc<GridManager>,
|
||||
) -> Arc<FolderManager> {
|
||||
let user: Arc<dyn WorkspaceUser> = Arc::new(WorkspaceUserImpl(user_session.clone()));
|
||||
@ -63,7 +63,7 @@ impl FolderDepsResolver {
|
||||
}
|
||||
|
||||
fn make_view_data_processor(
|
||||
text_block_manager: Arc<TextBlockManager>,
|
||||
text_block_manager: Arc<TextEditorManager>,
|
||||
grid_manager: Arc<GridManager>,
|
||||
) -> ViewDataProcessorMap {
|
||||
let mut map: HashMap<ViewDataTypePB, Arc<dyn ViewDataProcessor + Send + Sync>> = HashMap::new();
|
||||
@ -135,7 +135,7 @@ impl WSMessageReceiver for FolderWSMessageReceiverImpl {
|
||||
}
|
||||
}
|
||||
|
||||
struct TextBlockViewDataProcessor(Arc<TextBlockManager>);
|
||||
struct TextBlockViewDataProcessor(Arc<TextEditorManager>);
|
||||
impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
fn initialize(&self) -> FutureResult<(), FlowyError> {
|
||||
let manager = self.0.clone();
|
||||
@ -147,7 +147,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
let view_id = view_id.to_string();
|
||||
let manager = self.0.clone();
|
||||
FutureResult::new(async move {
|
||||
let _ = manager.create_block(view_id, repeated_revision).await?;
|
||||
let _ = manager.create_text_block(view_id, repeated_revision).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -156,7 +156,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
let manager = self.0.clone();
|
||||
let view_id = view_id.to_string();
|
||||
FutureResult::new(async move {
|
||||
let _ = manager.delete_block(view_id)?;
|
||||
let _ = manager.close_text_editor(view_id)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -165,7 +165,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
let manager = self.0.clone();
|
||||
let view_id = view_id.to_string();
|
||||
FutureResult::new(async move {
|
||||
let _ = manager.close_block(view_id)?;
|
||||
let _ = manager.close_text_editor(view_id)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@ -174,7 +174,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
let view_id = view_id.to_string();
|
||||
let manager = self.0.clone();
|
||||
FutureResult::new(async move {
|
||||
let editor = manager.open_block(view_id).await?;
|
||||
let editor = manager.open_text_editor(view_id).await?;
|
||||
let delta_bytes = Bytes::from(editor.delta_str().await?);
|
||||
Ok(delta_bytes)
|
||||
})
|
||||
@ -195,7 +195,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
let delta_data = Bytes::from(view_data);
|
||||
let repeated_revision: RepeatedRevision =
|
||||
Revision::initial_revision(&user_id, &view_id, delta_data.clone()).into();
|
||||
let _ = manager.create_block(view_id, repeated_revision).await?;
|
||||
let _ = manager.create_text_block(view_id, repeated_revision).await?;
|
||||
Ok(delta_data)
|
||||
})
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver};
|
||||
use flowy_sync::entities::ws_data::ClientRevisionWSData;
|
||||
use flowy_text_block::{
|
||||
errors::{internal_error, FlowyError},
|
||||
BlockCloudService, TextBlockManager, TextBlockUser,
|
||||
TextEditorCloudService, TextEditorManager, TextEditorUser,
|
||||
};
|
||||
use flowy_user::services::UserSession;
|
||||
use futures_core::future::BoxFuture;
|
||||
@ -23,15 +23,15 @@ impl TextBlockDepsResolver {
|
||||
ws_conn: Arc<FlowyWebSocketConnect>,
|
||||
user_session: Arc<UserSession>,
|
||||
server_config: &ClientServerConfiguration,
|
||||
) -> Arc<TextBlockManager> {
|
||||
) -> Arc<TextEditorManager> {
|
||||
let user = Arc::new(BlockUserImpl(user_session));
|
||||
let rev_web_socket = Arc::new(TextBlockWebSocket(ws_conn.clone()));
|
||||
let cloud_service: Arc<dyn BlockCloudService> = match local_server {
|
||||
let cloud_service: Arc<dyn TextEditorCloudService> = match local_server {
|
||||
None => Arc::new(BlockHttpCloudService::new(server_config.clone())),
|
||||
Some(local_server) => local_server,
|
||||
};
|
||||
|
||||
let manager = Arc::new(TextBlockManager::new(cloud_service, user, rev_web_socket));
|
||||
let manager = Arc::new(TextEditorManager::new(cloud_service, user, rev_web_socket));
|
||||
let receiver = Arc::new(DocumentWSMessageReceiverImpl(manager.clone()));
|
||||
ws_conn.add_ws_message_receiver(receiver).unwrap();
|
||||
|
||||
@ -40,7 +40,7 @@ impl TextBlockDepsResolver {
|
||||
}
|
||||
|
||||
struct BlockUserImpl(Arc<UserSession>);
|
||||
impl TextBlockUser for BlockUserImpl {
|
||||
impl TextEditorUser for BlockUserImpl {
|
||||
fn user_dir(&self) -> Result<String, FlowyError> {
|
||||
let dir = self.0.user_dir().map_err(|e| FlowyError::unauthorized().context(e))?;
|
||||
|
||||
@ -90,7 +90,7 @@ impl RevisionWebSocket for TextBlockWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
struct DocumentWSMessageReceiverImpl(Arc<TextBlockManager>);
|
||||
struct DocumentWSMessageReceiverImpl(Arc<TextEditorManager>);
|
||||
impl WSMessageReceiver for DocumentWSMessageReceiverImpl {
|
||||
fn source(&self) -> WSChannel {
|
||||
WSChannel::Document
|
||||
|
@ -11,7 +11,7 @@ use flowy_net::{
|
||||
local_server::LocalServer,
|
||||
ws::connection::{listen_on_websocket, FlowyWebSocketConnect},
|
||||
};
|
||||
use flowy_text_block::TextBlockManager;
|
||||
use flowy_text_block::TextEditorManager;
|
||||
use flowy_user::services::{notifier::UserStatus, UserSession, UserSessionConfig};
|
||||
use lib_dispatch::prelude::*;
|
||||
use lib_dispatch::runtime::tokio_default_runtime;
|
||||
@ -89,7 +89,7 @@ pub struct FlowySDK {
|
||||
#[allow(dead_code)]
|
||||
config: FlowySDKConfig,
|
||||
pub user_session: Arc<UserSession>,
|
||||
pub text_block_manager: Arc<TextBlockManager>,
|
||||
pub text_block_manager: Arc<TextEditorManager>,
|
||||
pub folder_manager: Arc<FolderManager>,
|
||||
pub grid_manager: Arc<GridManager>,
|
||||
pub dispatcher: Arc<EventDispatcher>,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use flowy_folder::manager::FolderManager;
|
||||
use flowy_grid::manager::GridManager;
|
||||
use flowy_net::ws::connection::FlowyWebSocketConnect;
|
||||
use flowy_text_block::TextBlockManager;
|
||||
use flowy_text_block::TextEditorManager;
|
||||
use flowy_user::services::UserSession;
|
||||
use lib_dispatch::prelude::Module;
|
||||
use std::sync::Arc;
|
||||
@ -11,7 +11,7 @@ pub fn mk_modules(
|
||||
folder_manager: &Arc<FolderManager>,
|
||||
grid_manager: &Arc<GridManager>,
|
||||
user_session: &Arc<UserSession>,
|
||||
text_block_manager: &Arc<TextBlockManager>,
|
||||
text_block_manager: &Arc<TextEditorManager>,
|
||||
) -> Vec<Module> {
|
||||
let user_module = mk_user_module(user_session.clone());
|
||||
let folder_module = mk_folder_module(folder_manager.clone());
|
||||
@ -43,6 +43,6 @@ fn mk_grid_module(grid_manager: Arc<GridManager>) -> Module {
|
||||
flowy_grid::event_map::create(grid_manager)
|
||||
}
|
||||
|
||||
fn mk_text_block_module(text_block_manager: Arc<TextBlockManager>) -> Module {
|
||||
fn mk_text_block_module(text_block_manager: Arc<TextEditorManager>) -> Module {
|
||||
flowy_text_block::event_map::create(text_block_manager)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use crate::web_socket::EditorCommandSender;
|
||||
use crate::{
|
||||
errors::FlowyError,
|
||||
queue::{EditBlockQueue, EditorCommand},
|
||||
TextBlockUser,
|
||||
TextEditorUser,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use flowy_error::{internal_error, FlowyResult};
|
||||
@ -24,7 +24,6 @@ use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
pub struct TextBlockEditor {
|
||||
pub doc_id: String,
|
||||
#[allow(dead_code)]
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
#[cfg(feature = "sync")]
|
||||
ws_manager: Arc<flowy_revision::RevisionWebSocketManager>,
|
||||
@ -35,7 +34,7 @@ impl TextBlockEditor {
|
||||
#[allow(unused_variables)]
|
||||
pub(crate) async fn new(
|
||||
doc_id: &str,
|
||||
user: Arc<dyn TextBlockUser>,
|
||||
user: Arc<dyn TextEditorUser>,
|
||||
mut rev_manager: RevisionManager,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
cloud_service: Arc<dyn RevisionCloudService>,
|
||||
@ -194,7 +193,7 @@ impl std::ops::Drop for TextBlockEditor {
|
||||
|
||||
// The edit queue will exit after the EditorCommandSender was dropped.
|
||||
fn spawn_edit_queue(
|
||||
user: Arc<dyn TextBlockUser>,
|
||||
user: Arc<dyn TextEditorUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
delta: TextDelta,
|
||||
) -> EditorCommandSender {
|
||||
|
@ -29,6 +29,52 @@ impl std::convert::From<i32> for ExportType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct EditPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub text_block_id: String,
|
||||
|
||||
// Encode in JSON format
|
||||
#[pb(index = 2)]
|
||||
pub operations: String,
|
||||
|
||||
// Encode in JSON format
|
||||
#[pb(index = 3)]
|
||||
pub delta: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EditParams {
|
||||
pub text_block_id: String,
|
||||
|
||||
// Encode in JSON format
|
||||
pub operations: String,
|
||||
|
||||
// Encode in JSON format
|
||||
pub delta: String,
|
||||
}
|
||||
|
||||
impl TryInto<EditParams> for EditPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<EditParams, Self::Error> {
|
||||
Ok(EditParams {
|
||||
text_block_id: self.text_block_id,
|
||||
operations: self.operations,
|
||||
delta: self.delta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct TextBlockPB {
|
||||
#[pb(index = 1)]
|
||||
pub text_block_id: String,
|
||||
|
||||
/// Encode in JSON format
|
||||
#[pb(index = 2)]
|
||||
pub snapshot: String,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct ExportPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
|
@ -1,39 +1,40 @@
|
||||
use crate::entities::{ExportDataPB, ExportParams, ExportPayloadPB};
|
||||
use crate::TextBlockManager;
|
||||
use crate::entities::{EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, TextBlockPB};
|
||||
use crate::TextEditorManager;
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_sync::entities::text_block::{TextBlockDeltaPB, TextBlockIdPB};
|
||||
use flowy_sync::entities::text_block::TextBlockIdPB;
|
||||
use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) async fn get_block_data_handler(
|
||||
pub(crate) async fn get_text_block_handler(
|
||||
data: Data<TextBlockIdPB>,
|
||||
manager: AppData<Arc<TextBlockManager>>,
|
||||
) -> DataResult<TextBlockDeltaPB, FlowyError> {
|
||||
let block_id: TextBlockIdPB = data.into_inner();
|
||||
let editor = manager.open_block(&block_id).await?;
|
||||
manager: AppData<Arc<TextEditorManager>>,
|
||||
) -> DataResult<TextBlockPB, FlowyError> {
|
||||
let text_block_id: TextBlockIdPB = data.into_inner();
|
||||
let editor = manager.open_text_editor(&text_block_id).await?;
|
||||
let delta_str = editor.delta_str().await?;
|
||||
data_result(TextBlockDeltaPB {
|
||||
block_id: block_id.into(),
|
||||
delta_str,
|
||||
data_result(TextBlockPB {
|
||||
text_block_id: text_block_id.into(),
|
||||
snapshot: delta_str,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_delta_handler(
|
||||
data: Data<TextBlockDeltaPB>,
|
||||
manager: AppData<Arc<TextBlockManager>>,
|
||||
) -> DataResult<TextBlockDeltaPB, FlowyError> {
|
||||
let block_delta = manager.receive_local_delta(data.into_inner()).await?;
|
||||
data_result(block_delta)
|
||||
pub(crate) async fn apply_edit_handler(
|
||||
data: Data<EditPayloadPB>,
|
||||
manager: AppData<Arc<TextEditorManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: EditParams = data.into_inner().try_into()?;
|
||||
let _ = manager.apply_edit(params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||
pub(crate) async fn export_handler(
|
||||
data: Data<ExportPayloadPB>,
|
||||
manager: AppData<Arc<TextBlockManager>>,
|
||||
manager: AppData<Arc<TextEditorManager>>,
|
||||
) -> DataResult<ExportDataPB, FlowyError> {
|
||||
let params: ExportParams = data.into_inner().try_into()?;
|
||||
let editor = manager.open_block(¶ms.view_id).await?;
|
||||
let editor = manager.open_text_editor(¶ms.view_id).await?;
|
||||
let delta_json = editor.delta_str().await?;
|
||||
data_result(ExportDataPB {
|
||||
data: delta_json,
|
||||
|
@ -1,16 +1,16 @@
|
||||
use crate::event_handler::*;
|
||||
use crate::TextBlockManager;
|
||||
use crate::TextEditorManager;
|
||||
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
||||
use lib_dispatch::prelude::Module;
|
||||
use std::sync::Arc;
|
||||
use strum_macros::Display;
|
||||
|
||||
pub fn create(block_manager: Arc<TextBlockManager>) -> Module {
|
||||
pub fn create(block_manager: Arc<TextEditorManager>) -> Module {
|
||||
let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(block_manager);
|
||||
|
||||
module = module
|
||||
.event(TextBlockEvent::GetBlockData, get_block_data_handler)
|
||||
.event(TextBlockEvent::ApplyDelta, apply_delta_handler)
|
||||
.event(TextBlockEvent::GetTextBlock, get_text_block_handler)
|
||||
.event(TextBlockEvent::ApplyEdit, apply_edit_handler)
|
||||
.event(TextBlockEvent::ExportDocument, export_handler);
|
||||
|
||||
module
|
||||
@ -19,11 +19,11 @@ pub fn create(block_manager: Arc<TextBlockManager>) -> Module {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
#[event_err = "FlowyError"]
|
||||
pub enum TextBlockEvent {
|
||||
#[event(input = "TextBlockIdPB", output = "TextBlockDeltaPB")]
|
||||
GetBlockData = 0,
|
||||
#[event(input = "TextBlockIdPB", output = "TextBlockPB")]
|
||||
GetTextBlock = 0,
|
||||
|
||||
#[event(input = "TextBlockDeltaPB", output = "TextBlockDeltaPB")]
|
||||
ApplyDelta = 1,
|
||||
#[event(input = "EditPayloadPB")]
|
||||
ApplyEdit = 1,
|
||||
|
||||
#[event(input = "ExportPayloadPB", output = "ExportDataPB")]
|
||||
ExportDocument = 2,
|
||||
|
@ -18,10 +18,10 @@ use crate::errors::FlowyError;
|
||||
use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB};
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
pub trait BlockCloudService: Send + Sync {
|
||||
fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>;
|
||||
pub trait TextEditorCloudService: Send + Sync {
|
||||
fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>;
|
||||
fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>;
|
||||
|
||||
fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>;
|
||||
fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::entities::EditParams;
|
||||
use crate::queue::TextBlockRevisionCompactor;
|
||||
use crate::{editor::TextBlockEditor, errors::FlowyError, BlockCloudService};
|
||||
use crate::{editor::TextBlockEditor, errors::FlowyError, TextEditorCloudService};
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use flowy_database::ConnectionPool;
|
||||
@ -16,30 +17,30 @@ use flowy_sync::entities::{
|
||||
use lib_infra::future::FutureResult;
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
pub trait TextBlockUser: Send + Sync {
|
||||
pub trait TextEditorUser: Send + Sync {
|
||||
fn user_dir(&self) -> Result<String, FlowyError>;
|
||||
fn user_id(&self) -> Result<String, FlowyError>;
|
||||
fn token(&self) -> Result<String, FlowyError>;
|
||||
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
|
||||
}
|
||||
|
||||
pub struct TextBlockManager {
|
||||
cloud_service: Arc<dyn BlockCloudService>,
|
||||
pub struct TextEditorManager {
|
||||
cloud_service: Arc<dyn TextEditorCloudService>,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
editor_map: Arc<TextBlockEditorMap>,
|
||||
user: Arc<dyn TextBlockUser>,
|
||||
editor_map: Arc<TextEditorMap>,
|
||||
user: Arc<dyn TextEditorUser>,
|
||||
}
|
||||
|
||||
impl TextBlockManager {
|
||||
impl TextEditorManager {
|
||||
pub fn new(
|
||||
cloud_service: Arc<dyn BlockCloudService>,
|
||||
text_block_user: Arc<dyn TextBlockUser>,
|
||||
cloud_service: Arc<dyn TextEditorCloudService>,
|
||||
text_block_user: Arc<dyn TextEditorUser>,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cloud_service,
|
||||
rev_web_socket,
|
||||
editor_map: Arc::new(TextBlockEditorMap::new()),
|
||||
editor_map: Arc::new(TextEditorMap::new()),
|
||||
user: text_block_user,
|
||||
}
|
||||
}
|
||||
@ -50,45 +51,47 @@ impl TextBlockManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)]
|
||||
pub async fn open_block<T: AsRef<str>>(&self, block_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> {
|
||||
let block_id = block_id.as_ref();
|
||||
tracing::Span::current().record("block_id", &block_id);
|
||||
self.get_block_editor(block_id).await
|
||||
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
||||
pub async fn open_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> {
|
||||
let editor_id = editor_id.as_ref();
|
||||
tracing::Span::current().record("editor_id", &editor_id);
|
||||
self.get_text_editor(editor_id).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)]
|
||||
pub fn close_block<T: AsRef<str>>(&self, block_id: T) -> Result<(), FlowyError> {
|
||||
let block_id = block_id.as_ref();
|
||||
tracing::Span::current().record("block_id", &block_id);
|
||||
self.editor_map.remove(block_id);
|
||||
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
||||
pub fn close_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<(), FlowyError> {
|
||||
let editor_id = editor_id.as_ref();
|
||||
tracing::Span::current().record("editor_id", &editor_id);
|
||||
self.editor_map.remove(editor_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, doc_id), fields(doc_id), err)]
|
||||
pub fn delete_block<T: AsRef<str>>(&self, doc_id: T) -> Result<(), FlowyError> {
|
||||
let doc_id = doc_id.as_ref();
|
||||
tracing::Span::current().record("doc_id", &doc_id);
|
||||
self.editor_map.remove(doc_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, delta), fields(doc_id = %delta.block_id), err)]
|
||||
#[tracing::instrument(level = "debug", skip(self, delta), err)]
|
||||
pub async fn receive_local_delta(&self, delta: TextBlockDeltaPB) -> Result<TextBlockDeltaPB, FlowyError> {
|
||||
let editor = self.get_block_editor(&delta.block_id).await?;
|
||||
let editor = self.get_text_editor(&delta.text_block_id).await?;
|
||||
let _ = editor.compose_local_delta(Bytes::from(delta.delta_str)).await?;
|
||||
let document_json = editor.delta_str().await?;
|
||||
let delta_str = editor.delta_str().await?;
|
||||
Ok(TextBlockDeltaPB {
|
||||
block_id: delta.block_id.clone(),
|
||||
delta_str: document_json,
|
||||
text_block_id: delta.text_block_id.clone(),
|
||||
delta_str,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_block<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
|
||||
let doc_id = doc_id.as_ref().to_owned();
|
||||
pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> {
|
||||
let editor = self.get_text_editor(¶ms.text_block_id).await?;
|
||||
let _ = editor.compose_local_delta(Bytes::from(params.delta)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_text_block<T: AsRef<str>>(
|
||||
&self,
|
||||
text_block_id: T,
|
||||
revisions: RepeatedRevision,
|
||||
) -> FlowyResult<()> {
|
||||
let doc_id = text_block_id.as_ref().to_owned();
|
||||
let db_pool = self.user.db_pool()?;
|
||||
// Maybe we could save the block to disk without creating the RevisionManager
|
||||
let rev_manager = self.make_rev_manager(&doc_id, db_pool)?;
|
||||
let rev_manager = self.make_text_block_rev_manager(&doc_id, db_pool)?;
|
||||
let _ = rev_manager.reset_object(revisions).await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -110,26 +113,26 @@ impl TextBlockManager {
|
||||
}
|
||||
}
|
||||
|
||||
impl TextBlockManager {
|
||||
async fn get_block_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> {
|
||||
impl TextEditorManager {
|
||||
async fn get_text_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> {
|
||||
match self.editor_map.get(block_id) {
|
||||
None => {
|
||||
let db_pool = self.user.db_pool()?;
|
||||
self.make_text_block_editor(block_id, db_pool).await
|
||||
self.make_text_editor(block_id, db_pool).await
|
||||
}
|
||||
Some(editor) => Ok(editor),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, pool), err)]
|
||||
async fn make_text_block_editor(
|
||||
async fn make_text_editor(
|
||||
&self,
|
||||
block_id: &str,
|
||||
pool: Arc<ConnectionPool>,
|
||||
) -> Result<Arc<TextBlockEditor>, FlowyError> {
|
||||
let user = self.user.clone();
|
||||
let token = self.user.token()?;
|
||||
let rev_manager = self.make_rev_manager(block_id, pool.clone())?;
|
||||
let rev_manager = self.make_text_block_rev_manager(block_id, pool.clone())?;
|
||||
let cloud_service = Arc::new(TextBlockRevisionCloudService {
|
||||
token,
|
||||
server: self.cloud_service.clone(),
|
||||
@ -140,7 +143,11 @@ impl TextBlockManager {
|
||||
Ok(doc_editor)
|
||||
}
|
||||
|
||||
fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> {
|
||||
fn make_text_block_rev_manager(
|
||||
&self,
|
||||
doc_id: &str,
|
||||
pool: Arc<ConnectionPool>,
|
||||
) -> Result<RevisionManager, FlowyError> {
|
||||
let user_id = self.user.user_id()?;
|
||||
let disk_cache = SQLiteTextBlockRevisionPersistence::new(&user_id, pool.clone());
|
||||
let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
|
||||
@ -161,7 +168,7 @@ impl TextBlockManager {
|
||||
|
||||
struct TextBlockRevisionCloudService {
|
||||
token: String,
|
||||
server: Arc<dyn BlockCloudService>,
|
||||
server: Arc<dyn TextEditorCloudService>,
|
||||
}
|
||||
|
||||
impl RevisionCloudService for TextBlockRevisionCloudService {
|
||||
@ -173,7 +180,7 @@ impl RevisionCloudService for TextBlockRevisionCloudService {
|
||||
let user_id = user_id.to_string();
|
||||
|
||||
FutureResult::new(async move {
|
||||
match server.read_block(&token, params).await? {
|
||||
match server.read_text_block(&token, params).await? {
|
||||
None => Err(FlowyError::record_not_found().context("Remote doesn't have this document")),
|
||||
Some(doc) => {
|
||||
let delta_data = Bytes::from(doc.text.clone());
|
||||
@ -193,36 +200,36 @@ impl RevisionCloudService for TextBlockRevisionCloudService {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextBlockEditorMap {
|
||||
pub struct TextEditorMap {
|
||||
inner: DashMap<String, Arc<TextBlockEditor>>,
|
||||
}
|
||||
|
||||
impl TextBlockEditorMap {
|
||||
impl TextEditorMap {
|
||||
fn new() -> Self {
|
||||
Self { inner: DashMap::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&self, block_id: &str, doc: &Arc<TextBlockEditor>) {
|
||||
if self.inner.contains_key(block_id) {
|
||||
log::warn!("Doc:{} already exists in cache", block_id);
|
||||
pub(crate) fn insert(&self, editor_id: &str, doc: &Arc<TextBlockEditor>) {
|
||||
if self.inner.contains_key(editor_id) {
|
||||
log::warn!("Doc:{} already exists in cache", editor_id);
|
||||
}
|
||||
self.inner.insert(block_id.to_string(), doc.clone());
|
||||
self.inner.insert(editor_id.to_string(), doc.clone());
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, block_id: &str) -> Option<Arc<TextBlockEditor>> {
|
||||
Some(self.inner.get(block_id)?.clone())
|
||||
pub(crate) fn get(&self, editor_id: &str) -> Option<Arc<TextBlockEditor>> {
|
||||
Some(self.inner.get(editor_id)?.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&self, block_id: &str) {
|
||||
if let Some(editor) = self.get(block_id) {
|
||||
pub(crate) fn remove(&self, editor_id: &str) {
|
||||
if let Some(editor) = self.get(editor_id) {
|
||||
editor.stop()
|
||||
}
|
||||
self.inner.remove(block_id);
|
||||
self.inner.remove(editor_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(web_socket, handlers))]
|
||||
fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextBlockEditorMap>) {
|
||||
fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextEditorMap>) {
|
||||
tokio::spawn(async move {
|
||||
let mut notify = web_socket.subscribe_state_changed().await;
|
||||
while let Ok(state) = notify.recv().await {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::web_socket::EditorCommandReceiver;
|
||||
use crate::TextBlockUser;
|
||||
use crate::TextEditorUser;
|
||||
use async_stream::stream;
|
||||
use bytes::Bytes;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
@ -23,14 +23,14 @@ use tokio::sync::{oneshot, RwLock};
|
||||
// serial.
|
||||
pub(crate) struct EditBlockQueue {
|
||||
document: Arc<RwLock<ClientDocument>>,
|
||||
user: Arc<dyn TextBlockUser>,
|
||||
user: Arc<dyn TextEditorUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
receiver: Option<EditorCommandReceiver>,
|
||||
}
|
||||
|
||||
impl EditBlockQueue {
|
||||
pub(crate) fn new(
|
||||
user: Arc<dyn TextBlockUser>,
|
||||
user: Arc<dyn TextEditorUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
delta: TextDelta,
|
||||
receiver: EditorCommandReceiver,
|
||||
|
@ -27,7 +27,7 @@ impl TextBlockEditorTest {
|
||||
let sdk = FlowySDKTest::default();
|
||||
let _ = sdk.init_user().await;
|
||||
let test = ViewTest::new_text_block_view(&sdk).await;
|
||||
let editor = sdk.text_block_manager.open_block(&test.view.id).await.unwrap();
|
||||
let editor = sdk.text_block_manager.open_text_editor(&test.view.id).await.unwrap();
|
||||
Self { sdk, editor }
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ pub struct ResetTextBlockParams {
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct TextBlockDeltaPB {
|
||||
#[pb(index = 1)]
|
||||
pub block_id: String,
|
||||
pub text_block_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub delta_str: String,
|
||||
|
@ -7,7 +7,7 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bytecount = "0.6.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
#protobuf = {version = "2.18.0"}
|
||||
#flowy-derive = { path = "../flowy-derive" }
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
|
@ -1,143 +0,0 @@
|
||||
use crate::core::attributes::Attributes;
|
||||
use crate::core::document::path::Path;
|
||||
use crate::core::{NodeBodyChangeset, NodeData};
|
||||
use crate::errors::OTError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "op")]
|
||||
pub enum NodeOperation {
|
||||
#[serde(rename = "insert")]
|
||||
Insert { path: Path, nodes: Vec<NodeData> },
|
||||
|
||||
#[serde(rename = "update")]
|
||||
UpdateAttributes {
|
||||
path: Path,
|
||||
attributes: Attributes,
|
||||
#[serde(rename = "oldAttributes")]
|
||||
old_attributes: Attributes,
|
||||
},
|
||||
|
||||
#[serde(rename = "update-body")]
|
||||
// #[serde(serialize_with = "serialize_edit_body")]
|
||||
// #[serde(deserialize_with = "deserialize_edit_body")]
|
||||
UpdateBody { path: Path, changeset: NodeBodyChangeset },
|
||||
|
||||
#[serde(rename = "delete")]
|
||||
Delete { path: Path, nodes: Vec<NodeData> },
|
||||
}
|
||||
|
||||
impl NodeOperation {
|
||||
pub fn path(&self) -> &Path {
|
||||
match self {
|
||||
NodeOperation::Insert { path, .. } => path,
|
||||
NodeOperation::UpdateAttributes { path, .. } => path,
|
||||
NodeOperation::Delete { path, .. } => path,
|
||||
NodeOperation::UpdateBody { path, .. } => path,
|
||||
}
|
||||
}
|
||||
pub fn invert(&self) -> NodeOperation {
|
||||
match self {
|
||||
NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
|
||||
path: path.clone(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
NodeOperation::UpdateAttributes {
|
||||
path,
|
||||
attributes,
|
||||
old_attributes,
|
||||
} => NodeOperation::UpdateAttributes {
|
||||
path: path.clone(),
|
||||
attributes: old_attributes.clone(),
|
||||
old_attributes: attributes.clone(),
|
||||
},
|
||||
NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
|
||||
path: path.clone(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody {
|
||||
path: path.clone(),
|
||||
changeset: body.inverted(),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn clone_with_new_path(&self, path: Path) -> NodeOperation {
|
||||
match self {
|
||||
NodeOperation::Insert { nodes, .. } => NodeOperation::Insert {
|
||||
path,
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
NodeOperation::UpdateAttributes {
|
||||
attributes,
|
||||
old_attributes,
|
||||
..
|
||||
} => NodeOperation::UpdateAttributes {
|
||||
path,
|
||||
attributes: attributes.clone(),
|
||||
old_attributes: old_attributes.clone(),
|
||||
},
|
||||
NodeOperation::Delete { nodes, .. } => NodeOperation::Delete {
|
||||
path,
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
NodeOperation::UpdateBody { path, changeset } => NodeOperation::UpdateBody {
|
||||
path: path.clone(),
|
||||
changeset: changeset.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn transform(a: &NodeOperation, b: &NodeOperation) -> NodeOperation {
|
||||
match a {
|
||||
NodeOperation::Insert { path: a_path, nodes } => {
|
||||
let new_path = Path::transform(a_path, b.path(), nodes.len() as i64);
|
||||
b.clone_with_new_path(new_path)
|
||||
}
|
||||
NodeOperation::Delete { path: a_path, nodes } => {
|
||||
let new_path = Path::transform(a_path, b.path(), nodes.len() as i64);
|
||||
b.clone_with_new_path(new_path)
|
||||
}
|
||||
_ => b.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct NodeOperationList {
|
||||
operations: Vec<NodeOperation>,
|
||||
}
|
||||
|
||||
impl NodeOperationList {
|
||||
pub fn into_inner(self) -> Vec<NodeOperation> {
|
||||
self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NodeOperationList {
|
||||
type Target = Vec<NodeOperation>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for NodeOperationList {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeOperationList {
|
||||
pub fn new(operations: Vec<NodeOperation>) -> Self {
|
||||
Self { operations }
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
|
||||
let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?;
|
||||
Ok(operation_list)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> {
|
||||
let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
|
||||
pub struct Path(pub Vec<usize>);
|
||||
|
||||
impl std::ops::Deref for Path {
|
||||
type Target = Vec<usize>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<usize> for Path {
|
||||
fn from(val: usize) -> Self {
|
||||
Path(vec![val])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&usize> for Path {
|
||||
fn from(val: &usize) -> Self {
|
||||
Path(vec![*val])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&Path> for Path {
|
||||
fn from(path: &Path) -> Self {
|
||||
path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<usize>> for Path {
|
||||
fn from(v: Vec<usize>) -> Self {
|
||||
Path(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Vec<usize>> for Path {
|
||||
fn from(values: &Vec<usize>) -> Self {
|
||||
Path(values.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[usize]> for Path {
|
||||
fn from(values: &[usize]) -> Self {
|
||||
Path(values.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl Path {
|
||||
// delta is default to be 1
|
||||
pub fn transform(pre_insert_path: &Path, b: &Path, offset: i64) -> Path {
|
||||
if pre_insert_path.len() > b.len() {
|
||||
return b.clone();
|
||||
}
|
||||
if pre_insert_path.is_empty() || b.is_empty() {
|
||||
return b.clone();
|
||||
}
|
||||
// check the prefix
|
||||
for i in 0..(pre_insert_path.len() - 1) {
|
||||
if pre_insert_path.0[i] != b.0[i] {
|
||||
return b.clone();
|
||||
}
|
||||
}
|
||||
let mut prefix: Vec<usize> = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into();
|
||||
let mut suffix: Vec<usize> = b.0[pre_insert_path.0.len()..].into();
|
||||
let prev_insert_last: usize = *pre_insert_path.0.last().unwrap();
|
||||
let b_at_index = b.0[pre_insert_path.0.len() - 1];
|
||||
if prev_insert_last <= b_at_index {
|
||||
prefix.push(((b_at_index as i64) + offset) as usize);
|
||||
} else {
|
||||
prefix.push(b_at_index);
|
||||
}
|
||||
prefix.append(&mut suffix);
|
||||
|
||||
Path(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::Path;
|
||||
#[test]
|
||||
fn path_transform_test_1() {
|
||||
assert_eq!(
|
||||
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 1) }.0,
|
||||
vec![0, 2]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 5) }.0,
|
||||
vec![0, 6]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_transform_test_2() {
|
||||
assert_eq!(
|
||||
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2]), 1) }.0,
|
||||
vec![0, 3]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_transform_test_3() {
|
||||
assert_eq!(
|
||||
{ Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2, 7, 8, 9]), 1) }.0,
|
||||
vec![0, 3, 7, 8, 9]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_transform_no_changed_test() {
|
||||
assert_eq!(
|
||||
{ Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 0, 7, 8, 9]), 1) }.0,
|
||||
vec![0, 0, 7, 8, 9]
|
||||
);
|
||||
assert_eq!(
|
||||
{ Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 1]), 1) }.0,
|
||||
vec![0, 1]
|
||||
);
|
||||
assert_eq!(
|
||||
{ Path::transform(&Path(vec![1, 1]), &Path(vec![1, 0]), 1) }.0,
|
||||
vec![1, 0]
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
pub mod attributes;
|
||||
mod delta;
|
||||
mod document;
|
||||
mod interval;
|
||||
mod node_tree;
|
||||
mod ot_str;
|
||||
|
||||
pub use attributes::*;
|
||||
pub use delta::operation::*;
|
||||
pub use delta::*;
|
||||
pub use document::*;
|
||||
pub use interval::*;
|
||||
pub use node_tree::*;
|
||||
pub use ot_str::*;
|
||||
|
@ -2,14 +2,14 @@
|
||||
|
||||
mod node;
|
||||
mod node_serde;
|
||||
mod node_tree;
|
||||
mod operation;
|
||||
mod operation_serde;
|
||||
mod path;
|
||||
mod transaction;
|
||||
mod tree;
|
||||
|
||||
pub use node::*;
|
||||
pub use node_tree::*;
|
||||
pub use operation::*;
|
||||
pub use path::*;
|
||||
pub use transaction::*;
|
||||
pub use tree::*;
|
@ -6,7 +6,7 @@ use crate::errors::OTError;
|
||||
use crate::text_delta::TextDelta;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct NodeData {
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: String,
|
174
shared-lib/lib-ot/src/core/node_tree/operation.rs
Normal file
174
shared-lib/lib-ot/src/core/node_tree/operation.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use crate::core::attributes::Attributes;
|
||||
use crate::core::{NodeBodyChangeset, NodeData, Path};
|
||||
use crate::errors::OTError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "op")]
|
||||
pub enum NodeOperation {
|
||||
#[serde(rename = "insert")]
|
||||
Insert { path: Path, nodes: Vec<NodeData> },
|
||||
|
||||
#[serde(rename = "update-attribute")]
|
||||
UpdateAttributes {
|
||||
path: Path,
|
||||
new: Attributes,
|
||||
old: Attributes,
|
||||
},
|
||||
|
||||
#[serde(rename = "update-body")]
|
||||
// #[serde(serialize_with = "serialize_edit_body")]
|
||||
// #[serde(deserialize_with = "deserialize_edit_body")]
|
||||
UpdateBody { path: Path, changeset: NodeBodyChangeset },
|
||||
|
||||
#[serde(rename = "delete")]
|
||||
Delete { path: Path, nodes: Vec<NodeData> },
|
||||
}
|
||||
|
||||
impl NodeOperation {
|
||||
pub fn get_path(&self) -> &Path {
|
||||
match self {
|
||||
NodeOperation::Insert { path, .. } => path,
|
||||
NodeOperation::UpdateAttributes { path, .. } => path,
|
||||
NodeOperation::Delete { path, .. } => path,
|
||||
NodeOperation::UpdateBody { path, .. } => path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut_path(&mut self) -> &mut Path {
|
||||
match self {
|
||||
NodeOperation::Insert { path, .. } => path,
|
||||
NodeOperation::UpdateAttributes { path, .. } => path,
|
||||
NodeOperation::Delete { path, .. } => path,
|
||||
NodeOperation::UpdateBody { path, .. } => path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invert(&self) -> NodeOperation {
|
||||
match self {
|
||||
NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
|
||||
path: path.clone(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
NodeOperation::UpdateAttributes {
|
||||
path,
|
||||
new: attributes,
|
||||
old: old_attributes,
|
||||
} => NodeOperation::UpdateAttributes {
|
||||
path: path.clone(),
|
||||
new: old_attributes.clone(),
|
||||
old: attributes.clone(),
|
||||
},
|
||||
NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
|
||||
path: path.clone(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody {
|
||||
path: path.clone(),
|
||||
changeset: body.inverted(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the `other` operation can be applied to the version after applying the `self` operation.
|
||||
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
|
||||
/// For example, if the inserted position has been acquired by others, then it's needed to do the transform to
|
||||
/// make sure the inserted position is right.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `other`: The operation that is going to be transformed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lib_ot::core::{NodeDataBuilder, NodeOperation, Path};
|
||||
/// let node_1 = NodeDataBuilder::new("text_1").build();
|
||||
/// let node_2 = NodeDataBuilder::new("text_2").build();
|
||||
///
|
||||
/// let op_1 = NodeOperation::Insert {
|
||||
/// path: Path(vec![0, 1]),
|
||||
/// nodes: vec![node_1],
|
||||
/// };
|
||||
///
|
||||
/// let mut op_2 = NodeOperation::Insert {
|
||||
/// path: Path(vec![0, 1]),
|
||||
/// nodes: vec![node_2],
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text_2"}]}"#);
|
||||
///
|
||||
/// op_1.transform(&mut op_2);
|
||||
/// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
|
||||
///
|
||||
/// ```
|
||||
pub fn transform(&self, other: &mut NodeOperation) {
|
||||
match self {
|
||||
NodeOperation::Insert { path, nodes } => {
|
||||
let new_path = path.transform(other.get_path(), nodes.len());
|
||||
*other.get_mut_path() = new_path;
|
||||
}
|
||||
NodeOperation::Delete { path, nodes } => {
|
||||
let new_path = path.transform(other.get_path(), nodes.len());
|
||||
*other.get_mut_path() = new_path;
|
||||
}
|
||||
_ => {
|
||||
// Only insert/delete will change the path.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct NodeOperationList {
|
||||
operations: Vec<Rc<NodeOperation>>,
|
||||
}
|
||||
|
||||
impl NodeOperationList {
|
||||
pub fn into_inner(self) -> Vec<Rc<NodeOperation>> {
|
||||
self.operations
|
||||
}
|
||||
|
||||
pub fn add_op(&mut self, operation: NodeOperation) {
|
||||
self.operations.push(Rc::new(operation));
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NodeOperationList {
|
||||
type Target = Vec<Rc<NodeOperation>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for NodeOperationList {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.operations
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Vec<NodeOperation>> for NodeOperationList {
|
||||
fn from(operations: Vec<NodeOperation>) -> Self {
|
||||
Self::new(operations)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeOperationList {
|
||||
pub fn new(operations: Vec<NodeOperation>) -> Self {
|
||||
Self {
|
||||
operations: operations.into_iter().map(Rc::new).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
|
||||
let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?;
|
||||
Ok(operation_list)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> {
|
||||
let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
190
shared-lib/lib-ot/src/core/node_tree/path.rs
Normal file
190
shared-lib/lib-ot/src/core/node_tree/path.rs
Normal file
@ -0,0 +1,190 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The `Path` represents as a path to reference to the node in the `NodeTree`.
|
||||
/// ┌─────────┐
|
||||
/// │ Root │
|
||||
/// └─────────┼──────────┐
|
||||
/// │0: Node A │
|
||||
/// └──────────┼────────────┐
|
||||
/// │0: Node A-1 │
|
||||
/// ├────────────┤
|
||||
/// │1: Node A-2 │
|
||||
/// ┌──────────┼────────────┘
|
||||
/// │1: Node B │
|
||||
/// └──────────┼────────────┐
|
||||
/// │0: Node B-1 │
|
||||
/// ├────────────┤
|
||||
/// │1: Node B-2 │
|
||||
/// ┌──────────┼────────────┘
|
||||
/// │2: Node C │
|
||||
/// └──────────┘
|
||||
///
|
||||
/// The path of Node A will be [0]
|
||||
/// The path of Node A-1 will be [0,0]
|
||||
/// The path of Node A-2 will be [0,1]
|
||||
/// The path of Node B-2 will be [1,1]
|
||||
#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
|
||||
pub struct Path(pub Vec<usize>);
|
||||
|
||||
impl std::ops::Deref for Path {
|
||||
type Target = Vec<usize>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<usize> for Path {
|
||||
fn from(val: usize) -> Self {
|
||||
Path(vec![val])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&usize> for Path {
|
||||
fn from(val: &usize) -> Self {
|
||||
Path(vec![*val])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&Path> for Path {
|
||||
fn from(path: &Path) -> Self {
|
||||
path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<usize>> for Path {
|
||||
fn from(v: Vec<usize>) -> Self {
|
||||
Path(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Vec<usize>> for Path {
|
||||
fn from(values: &Vec<usize>) -> Self {
|
||||
Path(values.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[usize]> for Path {
|
||||
fn from(values: &[usize]) -> Self {
|
||||
Path(values.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl Path {
|
||||
/// Calling this function if there are two changes want to modify the same path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `other`: the path that need to be transformed
|
||||
/// * `offset`: represents the len of nodes referenced by the current path
|
||||
///
|
||||
/// If two changes modify the same path or the path was shared by them. Then it needs to do the
|
||||
/// transformation to make sure the changes are applied to the right path.
|
||||
///
|
||||
/// returns: the path represents the position that the other path reference to.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lib_ot::core::Path;
|
||||
/// let path = Path(vec![0, 1]);
|
||||
/// for (old_path, len_of_nodes, expected_path) in vec![
|
||||
/// // Try to modify the path [0, 1], but someone has inserted one element before the
|
||||
/// // current path [0,1] in advance. That causes the modified path [0,1] to no longer
|
||||
/// // valid. It needs to do the transformation to get the right path.
|
||||
/// //
|
||||
/// // [0,2] is the path you want to modify.
|
||||
/// (Path(vec![0, 1]), 1, Path(vec![0, 2])),
|
||||
/// (Path(vec![0, 1]), 5, Path(vec![0, 6])),
|
||||
/// (Path(vec![0, 2]), 1, Path(vec![0, 3])),
|
||||
/// // Try to modify the path [0, 2,3,4], but someone has inserted one element before the
|
||||
/// // current path [0,1] in advance. That cause the prefix path [0,2] of [0,2,3,4]
|
||||
/// // no longer valid.
|
||||
/// // It needs to do the transformation to get the right path. So [0,2] is transformed to [0,3]
|
||||
/// // and the suffix [3,4] of the [0,2,3,4] remains the same. So the transformed result is
|
||||
/// //
|
||||
/// // [0,3,3,4]
|
||||
/// (Path(vec![0, 2, 3, 4]), 1, Path(vec![0, 3, 3, 4])),
|
||||
/// ] {
|
||||
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
|
||||
/// }
|
||||
/// // The path remains the same in the following test. Because the shared path is not changed.
|
||||
/// let path = Path(vec![0, 1, 2]);
|
||||
/// for (old_path, len_of_nodes, expected_path) in vec![
|
||||
/// // Try to modify the path [0,0,0,1,2], but someone has inserted one element
|
||||
/// // before [0,1,2]. [0,0,0,1,2] and [0,1,2] share the same path [0,x], because
|
||||
/// // the element was inserted at [0,1,2] that didn't affect the shared path [0, x].
|
||||
/// // So, after the transformation, the path is not changed.
|
||||
/// (Path(vec![0, 0, 0, 1, 2]), 1, Path(vec![0, 0, 0, 1, 2])),
|
||||
/// (Path(vec![0, 1]), 1, Path(vec![0, 1])),
|
||||
/// ] {
|
||||
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
|
||||
/// }
|
||||
///
|
||||
/// let path = Path(vec![1, 1]);
|
||||
/// for (old_path, len_of_nodes, expected_path) in vec![(Path(vec![1, 0]), 1, Path(vec![1, 0]))] {
|
||||
/// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
|
||||
/// }
|
||||
/// ```
|
||||
/// For example, client A and client B want to insert a node at the same index, the server applies
|
||||
/// the changes made by client B. But, before applying the client A's changes, server transforms
|
||||
/// the changes first in order to make sure that client A modify the right position. After that,
|
||||
/// the changes can be applied to the server.
|
||||
///
|
||||
/// ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
/// │ Client A │ │ Server │ │ Client B │
|
||||
/// └─────┬────┘ └─────┬────┘ └────┬─────┘
|
||||
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │
|
||||
/// │ │ Root │
|
||||
/// │ │ │ 0:A │ │
|
||||
/// │ │ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||||
/// │ │ ◀───────────────────────│
|
||||
/// │ │ Insert B at index 1 │
|
||||
/// │ │ │
|
||||
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │
|
||||
/// │ │ Root │
|
||||
/// │ │ │ 0:A │ │
|
||||
/// ├──────────────────────▶│ 1:B │
|
||||
/// │ Insert C at index 1 │ └ ─ ─ ─ ─ ─ ─ ─ ┘ │
|
||||
/// │ │ │
|
||||
/// │ │ transform index 1 to 2 │
|
||||
/// │ │ │
|
||||
/// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||||
/// │ │ Root │ │
|
||||
/// │ │ │ 0:A │
|
||||
/// ▼ ▼ 1:B │ ▼
|
||||
/// │ 2:C
|
||||
/// ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||
pub fn transform(&self, other: &Path, offset: usize) -> Path {
|
||||
if self.len() > other.len() {
|
||||
return other.clone();
|
||||
}
|
||||
if self.is_empty() || other.is_empty() {
|
||||
return other.clone();
|
||||
}
|
||||
for i in 0..(self.len() - 1) {
|
||||
if self.0[i] != other.0[i] {
|
||||
return other.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Splits the `Path` into two part. The suffix will contain the last element of the `Path`.
|
||||
let second_last_index = self.0.len() - 1;
|
||||
let mut prefix: Vec<usize> = self.0[0..second_last_index].into();
|
||||
let mut suffix: Vec<usize> = other.0[self.0.len()..].into();
|
||||
let last_value = *self.0.last().unwrap();
|
||||
|
||||
let other_second_last_value = other.0[second_last_index];
|
||||
|
||||
//
|
||||
if last_value <= other_second_last_value {
|
||||
prefix.push(other_second_last_value + offset);
|
||||
} else {
|
||||
prefix.push(other_second_last_value);
|
||||
}
|
||||
|
||||
// concat the prefix and suffix into a new path
|
||||
prefix.append(&mut suffix);
|
||||
Path(prefix)
|
||||
}
|
||||
}
|
@ -1,26 +1,57 @@
|
||||
use crate::core::attributes::Attributes;
|
||||
use crate::core::document::path::Path;
|
||||
use crate::core::{NodeData, NodeOperation, NodeTree};
|
||||
use crate::core::{NodeData, NodeOperation, NodeTree, Path};
|
||||
use crate::errors::OTError;
|
||||
use indextree::NodeId;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::{NodeBodyChangeset, NodeOperationList};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Transaction {
|
||||
operations: NodeOperationList,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new(operations: NodeOperationList) -> Transaction {
|
||||
Transaction { operations }
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn into_operations(self) -> Vec<NodeOperation> {
|
||||
pub fn from_operations<T: Into<NodeOperationList>>(operations: T) -> Self {
|
||||
Self {
|
||||
operations: operations.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_operations(self) -> Vec<Rc<NodeOperation>> {
|
||||
self.operations.into_inner()
|
||||
}
|
||||
|
||||
/// Make the `other` can be applied to the version after applying the `self` transaction.
|
||||
///
|
||||
/// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
|
||||
/// the operations of the transaction will be transformed into the conflict operations.
|
||||
pub fn transform(&self, other: &Transaction) -> Result<Transaction, OTError> {
|
||||
let mut new_transaction = other.clone();
|
||||
for other_operation in new_transaction.iter_mut() {
|
||||
let other_operation = Rc::make_mut(other_operation);
|
||||
for operation in self.operations.iter() {
|
||||
operation.transform(other_operation);
|
||||
}
|
||||
}
|
||||
Ok(new_transaction)
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, other: &Transaction) -> Result<(), OTError> {
|
||||
// For the moment, just append `other` operations to the end of `self`.
|
||||
for operation in other.operations.iter() {
|
||||
self.operations.push(operation.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Transaction {
|
||||
type Target = NodeOperationList;
|
||||
type Target = Vec<Rc<NodeOperation>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.operations
|
||||
@ -64,7 +95,7 @@ impl<'a> TransactionBuilder<'a> {
|
||||
/// let transaction = TransactionBuilder::new(&node_tree)
|
||||
/// .insert_nodes_at_path(0,vec![ NodeData::new("text_1"), NodeData::new("text_2")])
|
||||
/// .finalize();
|
||||
/// node_tree.apply(transaction).unwrap();
|
||||
/// node_tree.apply_transaction(transaction).unwrap();
|
||||
///
|
||||
/// node_tree.node_id_at_path(vec![0, 0]);
|
||||
/// ```
|
||||
@ -94,7 +125,7 @@ impl<'a> TransactionBuilder<'a> {
|
||||
/// let transaction = TransactionBuilder::new(&node_tree)
|
||||
/// .insert_node_at_path(0, NodeData::new("text"))
|
||||
/// .finalize();
|
||||
/// node_tree.apply(transaction).unwrap();
|
||||
/// node_tree.apply_transaction(transaction).unwrap();
|
||||
/// ```
|
||||
///
|
||||
pub fn insert_node_at_path<T: Into<Path>>(self, path: T, node: NodeData) -> Self {
|
||||
@ -112,10 +143,10 @@ impl<'a> TransactionBuilder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
self.operations.push(NodeOperation::UpdateAttributes {
|
||||
self.operations.add_op(NodeOperation::UpdateAttributes {
|
||||
path: path.clone(),
|
||||
attributes,
|
||||
old_attributes,
|
||||
new: attributes,
|
||||
old: old_attributes,
|
||||
});
|
||||
}
|
||||
None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
|
||||
@ -126,7 +157,7 @@ impl<'a> TransactionBuilder<'a> {
|
||||
pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self {
|
||||
match self.node_tree.node_id_at_path(path) {
|
||||
Some(_) => {
|
||||
self.operations.push(NodeOperation::UpdateBody {
|
||||
self.operations.add_op(NodeOperation::UpdateBody {
|
||||
path: path.clone(),
|
||||
changeset,
|
||||
});
|
||||
@ -148,7 +179,7 @@ impl<'a> TransactionBuilder<'a> {
|
||||
node = self.node_tree.following_siblings(node).next().unwrap();
|
||||
}
|
||||
|
||||
self.operations.push(NodeOperation::Delete {
|
||||
self.operations.add_op(NodeOperation::Delete {
|
||||
path: path.clone(),
|
||||
nodes: deleted_nodes,
|
||||
});
|
||||
@ -172,11 +203,11 @@ impl<'a> TransactionBuilder<'a> {
|
||||
}
|
||||
|
||||
pub fn push(mut self, op: NodeOperation) -> Self {
|
||||
self.operations.push(op);
|
||||
self.operations.add_op(op);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn finalize(self) -> Transaction {
|
||||
Transaction::new(self.operations)
|
||||
Transaction::from_operations(self.operations)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
use crate::core::attributes::Attributes;
|
||||
use crate::core::document::path::Path;
|
||||
use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Transaction};
|
||||
use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Path, Transaction};
|
||||
use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
|
||||
use indextree::{Arena, Children, FollowingSiblings, NodeId};
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::NodeOperationList;
|
||||
|
||||
@ -26,14 +26,13 @@ impl NodeTree {
|
||||
}
|
||||
|
||||
pub fn from_bytes(root_name: &str, bytes: Vec<u8>) -> Result<Self, OTError> {
|
||||
let operations = NodeOperationList::from_bytes(bytes)?.into_inner();
|
||||
let operations = NodeOperationList::from_bytes(bytes)?;
|
||||
Self::from_operations(root_name, operations)
|
||||
}
|
||||
|
||||
pub fn from_operations(root_name: &str, operations: Vec<NodeOperation>) -> Result<Self, OTError> {
|
||||
pub fn from_operations(root_name: &str, operations: NodeOperationList) -> Result<Self, OTError> {
|
||||
let mut node_tree = NodeTree::new(root_name);
|
||||
|
||||
for operation in operations {
|
||||
for operation in operations.into_inner().into_iter() {
|
||||
let _ = node_tree.apply_op(operation)?;
|
||||
}
|
||||
Ok(node_tree)
|
||||
@ -54,13 +53,14 @@ impl NodeTree {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::rc::Rc;
|
||||
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
|
||||
/// let nodes = vec![NodeData::new("text".to_string())];
|
||||
/// let root_path: Path = vec![0].into();
|
||||
/// let op = NodeOperation::Insert {path: root_path.clone(),nodes };
|
||||
///
|
||||
/// let mut node_tree = NodeTree::new("root");
|
||||
/// node_tree.apply_op(op).unwrap();
|
||||
/// node_tree.apply_op(Rc::new(op)).unwrap();
|
||||
/// let node_id = node_tree.node_id_at_path(&root_path).unwrap();
|
||||
/// let node_path = node_tree.path_from_node_id(node_id);
|
||||
/// debug_assert_eq!(node_path, root_path);
|
||||
@ -105,23 +105,25 @@ impl NodeTree {
|
||||
counter
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns the note_id at the position of the tree with id note_id
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `node_id`:
|
||||
/// * `index`:
|
||||
/// * `node_id`: the node id of the child's parent
|
||||
/// * `index`: index of the node in parent children list
|
||||
///
|
||||
/// returns: Option<NodeId>
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::rc::Rc;
|
||||
/// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
|
||||
/// let node_1 = NodeData::new("text".to_string());
|
||||
/// let inserted_path: Path = vec![0].into();
|
||||
///
|
||||
/// let mut node_tree = NodeTree::new("root");
|
||||
/// node_tree.apply_op(NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] }).unwrap();
|
||||
/// let op = NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] };
|
||||
/// node_tree.apply_op(Rc::new(op)).unwrap();
|
||||
///
|
||||
/// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap();
|
||||
/// assert_eq!(node_2.node_type, node_1.node_type);
|
||||
@ -137,6 +139,10 @@ impl NodeTree {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns all children whose parent node id is node_id
|
||||
///
|
||||
/// * `node_id`: the children's parent node id
|
||||
///
|
||||
pub fn children_from_node(&self, node_id: NodeId) -> Children<'_, Node> {
|
||||
node_id.children(&self.arena)
|
||||
}
|
||||
@ -159,7 +165,7 @@ impl NodeTree {
|
||||
node_id.following_siblings(&self.arena)
|
||||
}
|
||||
|
||||
pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> {
|
||||
pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> {
|
||||
let operations = transaction.into_operations();
|
||||
for operation in operations {
|
||||
self.apply_op(operation)?;
|
||||
@ -167,10 +173,15 @@ impl NodeTree {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_op(&mut self, op: NodeOperation) -> Result<(), OTError> {
|
||||
pub fn apply_op(&mut self, op: Rc<NodeOperation>) -> Result<(), OTError> {
|
||||
let op = match Rc::try_unwrap(op) {
|
||||
Ok(op) => op,
|
||||
Err(op) => op.as_ref().clone(),
|
||||
};
|
||||
|
||||
match op {
|
||||
NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes),
|
||||
NodeOperation::UpdateAttributes { path, attributes, .. } => self.update_attributes(&path, attributes),
|
||||
NodeOperation::UpdateAttributes { path, new, .. } => self.update_attributes(&path, new),
|
||||
NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset),
|
||||
NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes),
|
||||
}
|
||||
@ -216,7 +227,9 @@ impl NodeTree {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if index == parent.children(&self.arena).count() {
|
||||
// Append the node to the end of the children list if index greater or equal to the
|
||||
// length of the children.
|
||||
if index >= parent.children(&self.arena).count() {
|
||||
self.append_nodes(&parent, nodes);
|
||||
return Ok(());
|
||||
}
|
@ -2,5 +2,4 @@ use crate::core::{Attributes, Operation, OperationBuilder, Operations};
|
||||
|
||||
pub type TextDelta = Operations<Attributes>;
|
||||
pub type TextDeltaBuilder = OperationBuilder<Attributes>;
|
||||
|
||||
pub type TextOperation = Operation<Attributes>;
|
||||
|
@ -26,7 +26,8 @@ fn editor_deserialize_node_test() {
|
||||
test.run_scripts(vec![
|
||||
InsertNode {
|
||||
path,
|
||||
node: node.clone(),
|
||||
node_data: node.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNumberOfNodesAtPath { path: None, len: 1 },
|
||||
AssertNumberOfNodesAtPath {
|
||||
@ -41,11 +42,11 @@ fn editor_deserialize_node_test() {
|
||||
path: vec![0, 1].into(),
|
||||
expected: expected_delta,
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path: vec![0, 0].into(),
|
||||
expected: Some(node.children[0].clone()),
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path: vec![0, 3].into(),
|
||||
expected: Some(node.children[3].clone()),
|
||||
},
|
||||
|
@ -1,4 +1,6 @@
|
||||
use lib_ot::core::AttributeBuilder;
|
||||
use crate::node::script::NodeScript::*;
|
||||
use crate::node::script::NodeTest;
|
||||
use lib_ot::core::{AttributeBuilder, Node};
|
||||
use lib_ot::{
|
||||
core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path},
|
||||
text_delta::TextDeltaBuilder,
|
||||
@ -33,15 +35,14 @@ fn operation_insert_node_with_children_serde_test() {
|
||||
fn operation_update_node_attributes_serde_test() {
|
||||
let operation = NodeOperation::UpdateAttributes {
|
||||
path: Path(vec![0, 1]),
|
||||
attributes: AttributeBuilder::new().insert("bold", true).build(),
|
||||
old_attributes: AttributeBuilder::new().insert("bold", false).build(),
|
||||
new: AttributeBuilder::new().insert("bold", true).build(),
|
||||
old: AttributeBuilder::new().insert("bold", false).build(),
|
||||
};
|
||||
|
||||
let result = serde_json::to_string(&operation).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"op":"update","path":[0,1],"attributes":{"bold":true},"oldAttributes":{"bold":null}}"#
|
||||
r#"{"op":"update-attribute","path":[0,1],"new":{"bold":true},"old":{"bold":null}}"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,3 +70,166 @@ fn operation_update_node_body_deserialize_test() {
|
||||
let json_2 = serde_json::to_string(&operation).unwrap();
|
||||
assert_eq!(json_1, json_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_op_transform_test() {
|
||||
let node_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_2 = NodeDataBuilder::new("text_2").build();
|
||||
let op_1 = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![node_1],
|
||||
};
|
||||
|
||||
let mut insert_2 = NodeOperation::Insert {
|
||||
path: Path(vec![0, 1]),
|
||||
nodes: vec![node_2],
|
||||
};
|
||||
|
||||
// let mut node_tree = NodeTree::new("root");
|
||||
// node_tree.apply_op(insert_1.clone()).unwrap();
|
||||
|
||||
op_1.transform(&mut insert_2);
|
||||
let json = serde_json::to_string(&insert_2).unwrap();
|
||||
assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_transform_one_level_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_data_2 = NodeDataBuilder::new("text_2").build();
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let node_3: Node = node_data_3.clone().into();
|
||||
// 0: text_1
|
||||
// 1: text_2
|
||||
//
|
||||
// Insert a new operation with rev_id 1,but the rev_id:1 is already exist, so
|
||||
// it needs to be transformed.
|
||||
// 1:text_3 => 2:text_3
|
||||
//
|
||||
// 0: text_1
|
||||
// 1: text_2
|
||||
// 2: text_3
|
||||
//
|
||||
// If the rev_id of the insert operation is 3. then the tree will be:
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
// 2: text_2
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3,
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(node_3),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_insert_transform_multiple_level_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1")
|
||||
.add_node(NodeDataBuilder::new("text_1_1").build())
|
||||
.add_node(NodeDataBuilder::new("text_1_2").build())
|
||||
.build();
|
||||
|
||||
let node_data_2 = NodeDataBuilder::new("text_2")
|
||||
.add_node(NodeDataBuilder::new("text_2_1").build())
|
||||
.add_node(NodeDataBuilder::new("text_2_2").build())
|
||||
.build();
|
||||
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: Some(node_data_3.into()),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_delete_transform_path_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1").build();
|
||||
let node_data_2 = NodeDataBuilder::new("text_2").build();
|
||||
let node_data_3 = NodeDataBuilder::new("text_3").build();
|
||||
let node_3: Node = node_data_3.clone().into();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
// The node's in the tree will be:
|
||||
// 0: text_1
|
||||
// 2: text_2
|
||||
//
|
||||
// The insert action is happened concurrently with the delete action, because they
|
||||
// share the same rev_id. aka, 3. The delete action is want to delete the node at index 1,
|
||||
// but it was moved to index 2.
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_3,
|
||||
rev_id: 3,
|
||||
},
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
// 2: text_2
|
||||
//
|
||||
// The path of the delete action will be transformed to a new path that point to the text_2.
|
||||
// 1 -> 2
|
||||
DeleteNode {
|
||||
path: 1.into(),
|
||||
rev_id: 3,
|
||||
},
|
||||
// After perform the delete action, the tree will be:
|
||||
// 0: text_1
|
||||
// 1: text_3
|
||||
AssertNumberOfNodesAtPath { path: None, len: 2 },
|
||||
AssertNode {
|
||||
path: 1.into(),
|
||||
expected: Some(node_3),
|
||||
},
|
||||
AssertNode {
|
||||
path: 2.into(),
|
||||
expected: None,
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
@ -1,26 +1,58 @@
|
||||
use lib_ot::core::{Node, Transaction};
|
||||
use lib_ot::{
|
||||
core::attributes::Attributes,
|
||||
core::{NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder},
|
||||
text_delta::TextDelta,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub enum NodeScript {
|
||||
InsertNode { path: Path, node: NodeData },
|
||||
UpdateAttributes { path: Path, attributes: Attributes },
|
||||
UpdateBody { path: Path, changeset: NodeBodyChangeset },
|
||||
DeleteNode { path: Path },
|
||||
AssertNumberOfNodesAtPath { path: Option<Path>, len: usize },
|
||||
AssertNode { path: Path, expected: Option<NodeData> },
|
||||
AssertNodeDelta { path: Path, expected: TextDelta },
|
||||
InsertNode {
|
||||
path: Path,
|
||||
node_data: NodeData,
|
||||
rev_id: usize,
|
||||
},
|
||||
UpdateAttributes {
|
||||
path: Path,
|
||||
attributes: Attributes,
|
||||
},
|
||||
UpdateBody {
|
||||
path: Path,
|
||||
changeset: NodeBodyChangeset,
|
||||
},
|
||||
DeleteNode {
|
||||
path: Path,
|
||||
rev_id: usize,
|
||||
},
|
||||
AssertNumberOfNodesAtPath {
|
||||
path: Option<Path>,
|
||||
len: usize,
|
||||
},
|
||||
AssertNodeData {
|
||||
path: Path,
|
||||
expected: Option<NodeData>,
|
||||
},
|
||||
AssertNode {
|
||||
path: Path,
|
||||
expected: Option<Node>,
|
||||
},
|
||||
AssertNodeDelta {
|
||||
path: Path,
|
||||
expected: TextDelta,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct NodeTest {
|
||||
rev_id: usize,
|
||||
rev_operations: HashMap<usize, Transaction>,
|
||||
node_tree: NodeTree,
|
||||
}
|
||||
|
||||
impl NodeTest {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rev_id: 0,
|
||||
rev_operations: HashMap::new(),
|
||||
node_tree: NodeTree::new("root"),
|
||||
}
|
||||
}
|
||||
@ -33,40 +65,54 @@ impl NodeTest {
|
||||
|
||||
pub fn run_script(&mut self, script: NodeScript) {
|
||||
match script {
|
||||
NodeScript::InsertNode { path, node } => {
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
NodeScript::InsertNode {
|
||||
path,
|
||||
node_data: node,
|
||||
rev_id,
|
||||
} => {
|
||||
let mut transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.insert_node_at_path(path, node)
|
||||
.finalize();
|
||||
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
self.transform_transaction_if_need(&mut transaction, rev_id);
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
NodeScript::UpdateAttributes { path, attributes } => {
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.update_attributes_at_path(&path, attributes)
|
||||
.finalize();
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
NodeScript::UpdateBody { path, changeset } => {
|
||||
//
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.update_body_at_path(&path, changeset)
|
||||
.finalize();
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
NodeScript::DeleteNode { path } => {
|
||||
let transaction = TransactionBuilder::new(&self.node_tree)
|
||||
NodeScript::DeleteNode { path, rev_id } => {
|
||||
let mut transaction = TransactionBuilder::new(&self.node_tree)
|
||||
.delete_node_at_path(&path)
|
||||
.finalize();
|
||||
self.node_tree.apply(transaction).unwrap();
|
||||
self.transform_transaction_if_need(&mut transaction, rev_id);
|
||||
self.apply_transaction(transaction);
|
||||
}
|
||||
NodeScript::AssertNode { path, expected } => {
|
||||
let node_id = self.node_tree.node_id_at_path(path);
|
||||
if expected.is_none() && node_id.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let node = self.node_tree.get_node(node_id.unwrap()).cloned();
|
||||
assert_eq!(node, expected);
|
||||
}
|
||||
NodeScript::AssertNodeData { path, expected } => {
|
||||
let node_id = self.node_tree.node_id_at_path(path);
|
||||
|
||||
match node_id {
|
||||
None => assert!(node_id.is_none()),
|
||||
Some(node_id) => {
|
||||
let node_data = self.node_tree.get_node(node_id).cloned();
|
||||
assert_eq!(node_data, expected.map(|e| e.into()));
|
||||
let node = self.node_tree.get_node(node_id).cloned();
|
||||
assert_eq!(node, expected.map(|e| e.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,4 +140,19 @@ impl NodeTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transaction(&mut self, transaction: Transaction) {
|
||||
self.rev_id += 1;
|
||||
self.rev_operations.insert(self.rev_id, transaction.clone());
|
||||
self.node_tree.apply_transaction(transaction).unwrap();
|
||||
}
|
||||
|
||||
fn transform_transaction_if_need(&mut self, transaction: &mut Transaction, rev_id: usize) {
|
||||
if self.rev_id >= rev_id {
|
||||
for rev_id in rev_id..=self.rev_id {
|
||||
let old_transaction = self.rev_operations.get(&rev_id).unwrap();
|
||||
*transaction = old_transaction.transform(transaction).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,10 @@ fn node_insert_test() {
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone(),
|
||||
node: inserted_node.clone(),
|
||||
node_data: inserted_node.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path,
|
||||
expected: Some(inserted_node),
|
||||
},
|
||||
@ -32,9 +33,10 @@ fn node_insert_node_with_children_test() {
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone(),
|
||||
node: inserted_node.clone(),
|
||||
node_data: inserted_node.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path,
|
||||
expected: Some(inserted_node),
|
||||
},
|
||||
@ -57,25 +59,28 @@ fn node_insert_multi_nodes_test() {
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path_1.clone(),
|
||||
node: node_1.clone(),
|
||||
node_data: node_1.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: path_2.clone(),
|
||||
node: node_2.clone(),
|
||||
node_data: node_2.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: path_3.clone(),
|
||||
node: node_3.clone(),
|
||||
node_data: node_3.clone(),
|
||||
rev_id: 3,
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path: path_1,
|
||||
expected: Some(node_1),
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path: path_2,
|
||||
expected: Some(node_2),
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path: path_3,
|
||||
expected: Some(node_3),
|
||||
},
|
||||
@ -96,48 +101,145 @@ fn node_insert_node_in_ordered_nodes_test() {
|
||||
let path_3: Path = 2.into();
|
||||
let node_3 = NodeData::new("text_3");
|
||||
|
||||
let path_4: Path = 3.into();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path_1.clone(),
|
||||
node: node_1.clone(),
|
||||
node_data: node_1.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: path_2.clone(),
|
||||
node: node_2_1.clone(),
|
||||
node_data: node_2_1.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
InsertNode {
|
||||
path: path_3.clone(),
|
||||
node: node_3.clone(),
|
||||
node_data: node_3,
|
||||
rev_id: 3,
|
||||
},
|
||||
// 0:note_1 , 1: note_2_1, 2: note_3
|
||||
// 0:text_1
|
||||
// 1:text_2_1
|
||||
// 2:text_3
|
||||
InsertNode {
|
||||
path: path_2.clone(),
|
||||
node: node_2_2.clone(),
|
||||
node_data: node_2_2.clone(),
|
||||
rev_id: 4,
|
||||
},
|
||||
// 0:note_1 , 1:note_2_2, 2: note_2_1, 3: note_3
|
||||
AssertNode {
|
||||
// 0:text_1
|
||||
// 1:text_2_2
|
||||
// 2:text_2_1
|
||||
// 3:text_3
|
||||
AssertNodeData {
|
||||
path: path_1,
|
||||
expected: Some(node_1),
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path: path_2,
|
||||
expected: Some(node_2_2),
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path: path_3,
|
||||
expected: Some(node_2_1),
|
||||
},
|
||||
AssertNode {
|
||||
path: path_4,
|
||||
expected: Some(node_3),
|
||||
},
|
||||
AssertNumberOfNodesAtPath { path: None, len: 4 },
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_insert_nested_nodes_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1_1 = NodeDataBuilder::new("text_1_1").build();
|
||||
let node_data_1_2 = NodeDataBuilder::new("text_1_2").build();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1")
|
||||
.add_node(node_data_1_1.clone())
|
||||
.add_node(node_data_1_2.clone())
|
||||
.build();
|
||||
|
||||
let node_data_2_1 = NodeDataBuilder::new("text_2_1").build();
|
||||
let node_data_2_2 = NodeDataBuilder::new("text_2_2").build();
|
||||
let node_data_2 = NodeDataBuilder::new("text_2")
|
||||
.add_node(node_data_2_1.clone())
|
||||
.add_node(node_data_2_2.clone())
|
||||
.build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
InsertNode {
|
||||
path: 1.into(),
|
||||
node_data: node_data_2,
|
||||
rev_id: 2,
|
||||
},
|
||||
// the tree will be:
|
||||
// 0:text_1
|
||||
// 0:text_1_1
|
||||
// 1:text_1_2
|
||||
// 1:text_2
|
||||
// 0:text_2_1
|
||||
// 1:text_2_2
|
||||
AssertNode {
|
||||
path: vec![0, 0].into(),
|
||||
expected: Some(node_data_1_1.into()),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![0, 1].into(),
|
||||
expected: Some(node_data_1_2.into()),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 0].into(),
|
||||
expected: Some(node_data_2_1.into()),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 1].into(),
|
||||
expected: Some(node_data_2_2.into()),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_insert_node_before_existing_nested_nodes_test() {
|
||||
let mut test = NodeTest::new();
|
||||
let node_data_1_1 = NodeDataBuilder::new("text_1_1").build();
|
||||
let node_data_1_2 = NodeDataBuilder::new("text_1_2").build();
|
||||
let node_data_1 = NodeDataBuilder::new("text_1")
|
||||
.add_node(node_data_1_1.clone())
|
||||
.add_node(node_data_1_2.clone())
|
||||
.build();
|
||||
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: node_data_1,
|
||||
rev_id: 1,
|
||||
},
|
||||
// 0:text_1
|
||||
// 0:text_1_1
|
||||
// 1:text_1_2
|
||||
InsertNode {
|
||||
path: 0.into(),
|
||||
node_data: NodeDataBuilder::new("text_0").build(),
|
||||
rev_id: 2,
|
||||
},
|
||||
// 0:text_0
|
||||
// 1:text_1
|
||||
// 0:text_1_1
|
||||
// 1:text_1_2
|
||||
AssertNode {
|
||||
path: vec![1, 0].into(),
|
||||
expected: Some(node_data_1_1.into()),
|
||||
},
|
||||
AssertNode {
|
||||
path: vec![1, 1].into(),
|
||||
expected: Some(node_data_1_2.into()),
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
#[test]
|
||||
fn node_insert_with_attributes_test() {
|
||||
let mut test = NodeTest::new();
|
||||
@ -149,13 +251,14 @@ fn node_insert_with_attributes_test() {
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone(),
|
||||
node: inserted_node.clone(),
|
||||
node_data: inserted_node.clone(),
|
||||
rev_id: 1,
|
||||
},
|
||||
UpdateAttributes {
|
||||
path: path.clone(),
|
||||
attributes: inserted_node.attributes.clone(),
|
||||
},
|
||||
AssertNode {
|
||||
AssertNodeData {
|
||||
path,
|
||||
expected: Some(inserted_node),
|
||||
},
|
||||
@ -172,10 +275,14 @@ fn node_delete_test() {
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone(),
|
||||
node: inserted_node,
|
||||
node_data: inserted_node,
|
||||
rev_id: 1,
|
||||
},
|
||||
DeleteNode { path: path.clone() },
|
||||
AssertNode { path, expected: None },
|
||||
DeleteNode {
|
||||
path: path.clone(),
|
||||
rev_id: 2,
|
||||
},
|
||||
AssertNodeData { path, expected: None },
|
||||
];
|
||||
test.run_scripts(scripts);
|
||||
}
|
||||
@ -198,7 +305,8 @@ fn node_update_body_test() {
|
||||
let scripts = vec![
|
||||
InsertNode {
|
||||
path: path.clone(),
|
||||
node,
|
||||
node_data: node,
|
||||
rev_id: 1,
|
||||
},
|
||||
UpdateBody {
|
||||
path: path.clone(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user