feat: insert a new field to the left or right of an existing one (#4022)

* feat: allow inserting fields before or after a certain field

* fix: tauri build

* chore: implement frontend

* test: rust-lib tests

* test: integration test

* chore: point to temp collab rev

* chore: bump collab rev

* chore: fix tauri build

* chore: fix the tauri build, for real this time

* fix: new field editor show detail not general

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Richard Shiue
2023-11-29 04:42:53 +08:00
committed by GitHub
parent 38e3947b2d
commit 20b485bcfe
26 changed files with 478 additions and 256 deletions

View File

@ -10,8 +10,8 @@ import '../util/util.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid cell', () { group('edit grid cell:', () {
testWidgets('edit text cell', (tester) async { testWidgets('text', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -34,7 +34,7 @@ void main() {
// Make sure the text cells are filled with the right content when there are // Make sure the text cells are filled with the right content when there are
// multiple text cell // multiple text cell
testWidgets('edit multiple text cells', (tester) async { testWidgets('multiple text cells', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
await tester.createNewPageWithName( await tester.createNewPageWithName(
@ -73,7 +73,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('edit number cell', (tester) async { testWidgets('number', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -131,7 +131,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('edit checkbox cell', (tester) async { testWidgets('checkbox', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -149,7 +149,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('edit create time cell', (tester) async { testWidgets('created time', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -167,7 +167,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('edit last time cell', (tester) async { testWidgets('last modified time', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -185,7 +185,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('edit date time cell', (tester) async { testWidgets('date time', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -275,7 +275,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('edit single select cell', (tester) async { testWidgets('single select', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -347,7 +347,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('edit multi select cell', (tester) async { testWidgets('multi select', (tester) async {
final tags = [ final tags = [
'tag 1', 'tag 1',
'tag 2', 'tag 2',
@ -436,9 +436,8 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
});
testWidgets('edit checklist cell', (tester) async { testWidgets('checklist', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -539,4 +538,5 @@ void main() {
// check that the progress bar is not viisble // check that the progress bar is not viisble
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null); tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
}); });
});
} }

View File

@ -12,7 +12,7 @@ import '../util/util.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid page', () { group('grid field editor:', () {
testWidgets('rename existing field', (tester) async { testWidgets('rename existing field', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -98,7 +98,7 @@ void main() {
await tester.renameField('New field 1'); await tester.renameField('New field 1');
await tester.dismissFieldEditor(); await tester.dismissFieldEditor();
// Delete the field // duplicate the field
await tester.tapGridFieldWithName('New field 1'); await tester.tapGridFieldWithName('New field 1');
await tester.tapDuplicatePropertyButton(); await tester.tapDuplicatePropertyButton();
@ -106,6 +106,29 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('insert field on either side of a field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
// insert new field to the right
await tester.tapGridFieldWithName('Type');
await tester.tapInsertFieldButton(left: false, name: 'Right');
await tester.dismissFieldEditor();
await tester.findFieldWithName('Right');
// insert new field to the right
await tester.tapGridFieldWithName('Type');
await tester.tapInsertFieldButton(left: true, name: "Left");
await tester.dismissFieldEditor();
await tester.findFieldWithName('Left');
await tester.pumpAndSettle();
});
testWidgets('create checklist field', (tester) async { testWidgets('create checklist field', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();

View File

@ -765,6 +765,20 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(field); await tapButton(field);
} }
Future<void> tapInsertFieldButton({
required bool left,
required String name,
}) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
(left && widget.action == FieldAction.insertLeft ||
!left && widget.action == FieldAction.insertRight),
);
await tapButton(field);
await renameField(name);
}
/// Should call [tapGridFieldWithName] first. /// Should call [tapGridFieldWithName] first.
Future<void> tapHidePropertyButton() async { Future<void> tapHidePropertyButton() async {
final field = find.byWidgetPredicate( final field = find.byWidgetPredicate(

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart'; import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@ -25,11 +26,13 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
final FieldBackendService fieldService; final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService; final FieldSettingsBackendService fieldSettingsService;
final TypeOptionController typeOptionController; final TypeOptionController typeOptionController;
final void Function(String newFieldId)? onFieldInserted;
FieldEditorBloc({ FieldEditorBloc({
required this.viewId, required this.viewId,
required this.field, required this.field,
required this.fieldController, required this.fieldController,
this.onFieldInserted,
required FieldTypeOptionLoader loader, required FieldTypeOptionLoader loader,
}) : typeOptionController = TypeOptionController( }) : typeOptionController = TypeOptionController(
field: field, field: field,
@ -73,6 +76,28 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
final result = await fieldService.updateField(name: newName); final result = await fieldService.updateField(name: newName);
_logIfError(result); _logIfError(result);
}, },
insertLeft: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: viewId,
position: CreateFieldPosition.Before,
targetFieldId: field.id,
);
result.fold(
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id),
(err) => Log.error("Failed creating field $err"),
);
},
insertRight: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: viewId,
position: CreateFieldPosition.After,
targetFieldId: field.id,
);
result.fold(
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id),
(err) => Log.error("Failed creating field $err"),
);
},
toggleFieldVisibility: () async { toggleFieldVisibility: () async {
final currentVisibility = final currentVisibility =
state.field.visibility ?? FieldVisibility.AlwaysShown; state.field.visibility ?? FieldVisibility.AlwaysShown;
@ -122,6 +147,8 @@ class FieldEditorEvent with _$FieldEditorEvent {
const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) = const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) =
_SwitchFieldType; _SwitchFieldType;
const factory FieldEditorEvent.renameField(final String name) = _RenameField; const factory FieldEditorEvent.renameField(final String name) = _RenameField;
const factory FieldEditorEvent.insertLeft() = _InsertLeft;
const factory FieldEditorEvent.insertRight() = _InsertRight;
const factory FieldEditorEvent.toggleFieldVisibility() = const factory FieldEditorEvent.toggleFieldVisibility() =
_ToggleFieldVisiblity; _ToggleFieldVisiblity;
const factory FieldEditorEvent.deleteField() = _DeleteField; const factory FieldEditorEvent.deleteField() = _DeleteField;

View File

@ -26,11 +26,20 @@ class TypeOptionBackendService {
static Future<Either<TypeOptionPB, FlowyError>> createFieldTypeOption({ static Future<Either<TypeOptionPB, FlowyError>> createFieldTypeOption({
required String viewId, required String viewId,
FieldType fieldType = FieldType.RichText, FieldType fieldType = FieldType.RichText,
CreateFieldPosition position = CreateFieldPosition.End,
String? targetFieldId,
}) { }) {
final payload = CreateFieldPayloadPB.create() final payload = CreateFieldPayloadPB.create()
..viewId = viewId ..viewId = viewId
..fieldType = FieldType.RichText; ..fieldType = FieldType.RichText;
return DatabaseEventCreateTypeOption(payload).send(); if (position == CreateFieldPosition.Before ||
position == CreateFieldPosition.After && targetFieldId != null) {
payload.targetFieldId = targetFieldId!;
}
payload.fieldPosition = position;
return DatabaseEventCreateField(payload).send();
} }
} }

View File

@ -20,17 +20,17 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
}) : super(GridHeaderState.initial()) { }) : super(GridHeaderState.initial()) {
on<GridHeaderEvent>( on<GridHeaderEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.when(
initial: (_InitialHeader value) { initial: () {
_startListening(); _startListening();
add( add(
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos), GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
); );
}, },
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) { didReceiveFieldUpdate: (List<FieldInfo> fields) {
emit( emit(
state.copyWith( state.copyWith(
fields: value.fields fields: fields
.where( .where(
(element) => (element) =>
element.visibility != null && element.visibility != null &&
@ -40,8 +40,17 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
), ),
); );
}, },
moveField: (_MoveField value) async { startEditingField: (fieldId) {
await _moveField(value, emit); emit(state.copyWith(editingFieldId: fieldId));
},
startEditingNewField: (fieldId) {
emit(state.copyWith(editingFieldId: fieldId, newFieldId: fieldId));
},
endEditingField: () {
emit(state.copyWith(editingFieldId: null, newFieldId: null));
},
moveField: (field, fromIndex, toIndex) async {
await _moveField(field, fromIndex, toIndex, emit);
}, },
); );
}, },
@ -49,19 +58,17 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
} }
Future<void> _moveField( Future<void> _moveField(
_MoveField value, FieldPB field,
int fromIndex,
int toIndex,
Emitter<GridHeaderState> emit, Emitter<GridHeaderState> emit,
) async { ) async {
final fields = List<FieldInfo>.from(state.fields); final fields = List<FieldInfo>.from(state.fields);
fields.insert(value.toIndex, fields.removeAt(value.fromIndex)); fields.insert(toIndex, fields.removeAt(fromIndex));
emit(state.copyWith(fields: fields)); emit(state.copyWith(fields: fields));
final fieldService = final fieldService = FieldBackendService(viewId: viewId, fieldId: field.id);
FieldBackendService(viewId: viewId, fieldId: value.field.id); final result = await fieldService.moveField(fromIndex, toIndex);
final result = await fieldService.moveField(
value.fromIndex,
value.toIndex,
);
result.fold((l) {}, (err) => Log.error(err)); result.fold((l) {}, (err) => Log.error(err));
} }
@ -79,6 +86,11 @@ class GridHeaderEvent with _$GridHeaderEvent {
const factory GridHeaderEvent.initial() = _InitialHeader; const factory GridHeaderEvent.initial() = _InitialHeader;
const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldInfo> fields) = const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =
_DidReceiveFieldUpdate; _DidReceiveFieldUpdate;
const factory GridHeaderEvent.startEditingField(String fieldId) =
_StartEditingField;
const factory GridHeaderEvent.startEditingNewField(String fieldId) =
_StartEditingNewField;
const factory GridHeaderEvent.endEditingField() = _EndEditingField;
const factory GridHeaderEvent.moveField( const factory GridHeaderEvent.moveField(
FieldPB field, FieldPB field,
int fromIndex, int fromIndex,
@ -88,8 +100,12 @@ class GridHeaderEvent with _$GridHeaderEvent {
@freezed @freezed
class GridHeaderState with _$GridHeaderState { class GridHeaderState with _$GridHeaderState {
const factory GridHeaderState({required List<FieldInfo> fields}) = const factory GridHeaderState({
_GridHeaderState; required List<FieldInfo> fields,
required String? editingFieldId,
required String? newFieldId,
}) = _GridHeaderState;
factory GridHeaderState.initial() => const GridHeaderState(fields: []); factory GridHeaderState.initial() =>
const GridHeaderState(fields: [], editingFieldId: null, newFieldId: null);
} }

View File

@ -16,16 +16,27 @@ import 'field_editor.dart';
import 'field_type_extension.dart'; import 'field_type_extension.dart';
class GridFieldCell extends StatefulWidget { class GridFieldCell extends StatefulWidget {
final String viewId;
final FieldController fieldController;
final FieldInfo fieldInfo;
const GridFieldCell({ const GridFieldCell({
super.key, super.key,
required this.viewId, required this.viewId,
required this.fieldController, required this.fieldController,
required this.fieldInfo, required this.fieldInfo,
required this.onTap,
required this.onEditorOpened,
required this.onFieldInsertedOnEitherSide,
required this.isEditing,
required this.isNew,
}); });
final String viewId;
final FieldController fieldController;
final FieldInfo fieldInfo;
final VoidCallback onTap;
final VoidCallback onEditorOpened;
final void Function(String fieldId) onFieldInsertedOnEitherSide;
final bool isEditing;
final bool isNew;
@override @override
State<GridFieldCell> createState() => _GridFieldCellState(); State<GridFieldCell> createState() => _GridFieldCellState();
} }
@ -39,6 +50,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
super.initState(); super.initState();
popoverController = PopoverController(); popoverController = PopoverController();
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo); _bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
if (widget.isEditing) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
popoverController.show();
});
}
} }
@override @override
@ -46,6 +62,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) { if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) {
_bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo)); _bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo));
} }
if (widget.isEditing) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
popoverController.show();
});
}
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
@ -62,16 +83,20 @@ class _GridFieldCellState extends State<GridFieldCell> {
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
controller: popoverController, controller: popoverController,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
widget.onEditorOpened();
return FieldEditor( return FieldEditor(
viewId: widget.viewId, viewId: widget.viewId,
fieldController: widget.fieldController, fieldController: widget.fieldController,
field: widget.fieldInfo.field, field: widget.fieldInfo.field,
initialPage: FieldEditorPage.general, initialPage: widget.isNew
? FieldEditorPage.details
: FieldEditorPage.general,
onFieldInserted: widget.onFieldInsertedOnEitherSide,
); );
}, },
child: FieldCellButton( child: FieldCellButton(
field: widget.fieldInfo.field, field: widget.fieldInfo.field,
onTap: () => popoverController.show(), onTap: widget.onTap,
), ),
); );

View File

@ -30,6 +30,7 @@ class FieldEditor extends StatefulWidget {
final FieldController fieldController; final FieldController fieldController;
final FieldPB field; final FieldPB field;
final FieldEditorPage initialPage; final FieldEditorPage initialPage;
final void Function(String fieldId)? onFieldInserted;
const FieldEditor({ const FieldEditor({
super.key, super.key,
@ -37,6 +38,7 @@ class FieldEditor extends StatefulWidget {
required this.field, required this.field,
required this.fieldController, required this.fieldController,
this.initialPage = FieldEditorPage.details, this.initialPage = FieldEditorPage.details,
this.onFieldInserted,
}); });
@override @override
@ -61,6 +63,7 @@ class _FieldEditorState extends State<FieldEditor> {
viewId: widget.viewId, viewId: widget.viewId,
field: widget.field, field: widget.field,
fieldController: widget.fieldController, fieldController: widget.fieldController,
onFieldInserted: widget.onFieldInserted,
loader: FieldTypeOptionLoader( loader: FieldTypeOptionLoader(
viewId: widget.viewId, viewId: widget.viewId,
field: widget.field, field: widget.field,
@ -89,6 +92,10 @@ class _FieldEditorState extends State<FieldEditor> {
}, },
), ),
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.insertLeft),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.insertRight),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.toggleVisibility), _actionCell(FieldAction.toggleVisibility),
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.duplicate), _actionCell(FieldAction.duplicate),
@ -170,40 +177,56 @@ class FieldActionCell extends StatelessWidget {
), ),
onHover: (_) => popoverMutex?.close(), onHover: (_) => popoverMutex?.close(),
onTap: () => action.run(context, viewId, fieldInfo), onTap: () => action.run(context, viewId, fieldInfo),
leftIcon: FlowySvg( leftIcon: action.icon(
action.icon(fieldInfo), fieldInfo,
size: const Size.square(16), enable ? null : Theme.of(context).disabledColor,
color: enable ? null : Theme.of(context).disabledColor,
), ),
); );
} }
} }
enum FieldAction { enum FieldAction {
insertLeft,
insertRight,
toggleVisibility, toggleVisibility,
duplicate, duplicate,
delete, delete;
}
extension _FieldActionExtension on FieldAction { Widget icon(FieldInfo fieldInfo, Color? color) {
FlowySvgData icon(FieldInfo fieldInfo) { late final FlowySvgData svgData;
switch (this) { switch (this) {
case FieldAction.insertLeft:
svgData = FlowySvgs.arrow_s;
case FieldAction.insertRight:
svgData = FlowySvgs.arrow_s;
case FieldAction.toggleVisibility: case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null && if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) { fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
return FlowySvgs.show_m; svgData = FlowySvgs.show_m;
} else { } else {
return FlowySvgs.hide_s; svgData = FlowySvgs.hide_s;
} }
case FieldAction.duplicate: case FieldAction.duplicate:
return FlowySvgs.copy_s; svgData = FlowySvgs.copy_s;
case FieldAction.delete: case FieldAction.delete:
return FlowySvgs.delete_s; svgData = FlowySvgs.delete_s;
} }
final icon = FlowySvg(
svgData,
size: const Size.square(16),
color: color,
);
return this == FieldAction.insertRight
? Transform.flip(flipX: true, child: icon)
: icon;
} }
String title(FieldInfo fieldInfo) { String title(FieldInfo fieldInfo) {
switch (this) { switch (this) {
case FieldAction.insertLeft:
return LocaleKeys.grid_field_insertLeft.tr();
case FieldAction.insertRight:
return LocaleKeys.grid_field_insertRight.tr();
case FieldAction.toggleVisibility: case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null && if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) { fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
@ -220,6 +243,18 @@ extension _FieldActionExtension on FieldAction {
void run(BuildContext context, String viewId, FieldInfo fieldInfo) { void run(BuildContext context, String viewId, FieldInfo fieldInfo) {
switch (this) { switch (this) {
case FieldAction.insertLeft:
PopoverContainer.of(context).close();
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.insertLeft());
break;
case FieldAction.insertRight:
PopoverContainer.of(context).close();
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.insertRight());
break;
case FieldAction.toggleVisibility: case FieldAction.toggleVisibility:
PopoverContainer.of(context).close(); PopoverContainer.of(context).close();
context context

View File

@ -7,17 +7,14 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reorderables/reorderables.dart'; import 'package:reorderables/reorderables.dart';
import '../../../../application/field/type_option/type_option_service.dart'; import '../../../../application/field/type_option/type_option_service.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
import 'field_editor.dart';
import 'field_cell.dart'; import 'field_cell.dart';
class GridHeaderSliverAdaptor extends StatefulWidget { class GridHeaderSliverAdaptor extends StatefulWidget {
@ -93,7 +90,6 @@ class _GridHeaderState extends State<_GridHeader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<GridHeaderBloc, GridHeaderState>( return BlocBuilder<GridHeaderBloc, GridHeaderState>(
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) { builder: (context, state) {
final cells = state.fields final cells = state.fields
.map( .map(
@ -103,6 +99,17 @@ class _GridHeaderState extends State<_GridHeader> {
viewId: widget.viewId, viewId: widget.viewId,
fieldInfo: fieldInfo, fieldInfo: fieldInfo,
fieldController: widget.fieldController, fieldController: widget.fieldController,
onTap: () => context
.read<GridHeaderBloc>()
.add(GridHeaderEvent.startEditingField(fieldInfo.id)),
onFieldInsertedOnEitherSide: (fieldId) => context
.read<GridHeaderBloc>()
.add(GridHeaderEvent.startEditingNewField(fieldId)),
onEditorOpened: () => context
.read<GridHeaderBloc>()
.add(const GridHeaderEvent.endEditingField()),
isEditing: state.editingFieldId == fieldInfo.id,
isNew: state.newFieldId == fieldInfo.id,
) )
: MobileFieldButton( : MobileFieldButton(
key: _getKeyById(fieldInfo.id), key: _getKeyById(fieldInfo.id),
@ -184,38 +191,34 @@ class _CellTrailing extends StatelessWidget {
) )
: null, : null,
padding: GridSize.headerContentInsets, padding: GridSize.headerContentInsets,
child: CreateFieldButton(viewId: viewId), child: CreateFieldButton(
viewId: viewId,
onFieldCreated: (fieldId) => context
.read<GridHeaderBloc>()
.add(GridHeaderEvent.startEditingNewField(fieldId)),
),
); );
} }
} }
class CreateFieldButton extends StatefulWidget { class CreateFieldButton extends StatefulWidget {
final String viewId;
const CreateFieldButton({ const CreateFieldButton({
super.key, super.key,
required this.viewId, required this.viewId,
required this.onFieldCreated,
}); });
final String viewId;
final void Function(String fieldId) onFieldCreated;
@override @override
State<CreateFieldButton> createState() => _CreateFieldButtonState(); State<CreateFieldButton> createState() => _CreateFieldButtonState();
} }
class _CreateFieldButtonState extends State<CreateFieldButton> { class _CreateFieldButtonState extends State<CreateFieldButton> {
final popoverController = PopoverController();
late TypeOptionPB typeOption;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fieldController = return FlowyButton(
context.read<GridBloc>().databaseController.fieldController;
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithRightAligned,
asBarrier: true,
margin: EdgeInsets.zero,
constraints: BoxConstraints.loose(const Size(240, 600)),
triggerActions: PopoverTriggerFlags.none,
child: FlowyButton(
margin: PlatformExtension.isDesktop margin: PlatformExtension.isDesktop
? GridSize.cellContentInsets ? GridSize.cellContentInsets
: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), : const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
@ -223,8 +226,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
text: FlowyText.medium( text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(), LocaleKeys.grid_field_newProperty.tr(),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
), ),
hoverColor: AFThemeExtension.of(context).greyHover, hoverColor: AFThemeExtension.of(context).greyHover,
onTap: () async { onTap: () async {
@ -232,26 +234,14 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
viewId: widget.viewId, viewId: widget.viewId,
); );
result.fold( result.fold(
(l) { (typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id),
typeOption = l; (err) => Log.error("Failed to create field type option: $err"),
popoverController.show();
},
(r) => Log.error("Failed to create field type option: $r"),
); );
}, },
leftIcon: FlowySvg( leftIcon: FlowySvg(
FlowySvgs.add_s, FlowySvgs.add_s,
color: color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
), ),
),
popupBuilder: (BuildContext popoverContext) {
return FieldEditor(
viewId: widget.viewId,
fieldController: fieldController,
field: typeOption.field_2,
);
},
); );
} }
} }

View File

@ -880,7 +880,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -900,7 +900,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -930,7 +930,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-derive" name = "collab-derive"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -942,7 +942,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -962,7 +962,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-entity" name = "collab-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -976,7 +976,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -1018,7 +1018,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-persistence" name = "collab-persistence"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1040,7 +1040,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1067,7 +1067,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",

View File

@ -67,15 +67,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "509
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }

View File

@ -19,7 +19,7 @@ import {
DatabaseEventMoveField, DatabaseEventMoveField,
DatabaseEventGetFields, DatabaseEventGetFields,
DatabaseEventDeleteField, DatabaseEventDeleteField,
DatabaseEventCreateTypeOption, DatabaseEventCreateField,
DatabaseEventUpdateFieldSettings, DatabaseEventUpdateFieldSettings,
DatabaseEventGetAllFieldSettings, DatabaseEventGetAllFieldSettings,
} from '@/services/backend/events/flowy-database2'; } from '@/services/backend/events/flowy-database2';
@ -73,7 +73,7 @@ export async function createField(viewId: string, fieldType?: FieldType, data?:
type_option_data: data, type_option_data: data,
}); });
const result = await DatabaseEventCreateTypeOption(payload); const result = await DatabaseEventCreateField(payload);
if (result.ok === false) { if (result.ok === false) {
return Promise.reject('Failed to create field'); return Promise.reject('Failed to create field');

View File

@ -1,6 +1,6 @@
import { CreateFieldPayloadPB, FieldType, TypeOptionPathPB, UpdateFieldTypePayloadPB } from '@/services/backend'; import { CreateFieldPayloadPB, FieldType, TypeOptionPathPB, UpdateFieldTypePayloadPB } from '@/services/backend';
import { import {
DatabaseEventCreateTypeOption, DatabaseEventCreateField,
DatabaseEventGetTypeOption, DatabaseEventGetTypeOption,
DatabaseEventUpdateFieldType, DatabaseEventUpdateFieldType,
} from '@/services/backend/events/flowy-database2'; } from '@/services/backend/events/flowy-database2';
@ -11,7 +11,7 @@ export class TypeOptionBackendService {
createTypeOption = (fieldType: FieldType) => { createTypeOption = (fieldType: FieldType) => {
const payload = CreateFieldPayloadPB.fromObject({ view_id: this.viewId, field_type: fieldType }); const payload = CreateFieldPayloadPB.fromObject({ view_id: this.viewId, field_type: fieldType });
return DatabaseEventCreateTypeOption(payload); return DatabaseEventCreateField(payload);
}; };
getTypeOption = (fieldId: string, fieldType: FieldType) => { getTypeOption = (fieldId: string, fieldType: FieldType) => {

View File

@ -0,0 +1,3 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 5.5L3.5 8.5M3.5 8.5L6 11.5M3.5 8.5H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@ -730,7 +730,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -750,7 +750,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -780,7 +780,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-derive" name = "collab-derive"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -792,7 +792,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -812,7 +812,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-entity" name = "collab-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -826,7 +826,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -868,7 +868,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-persistence" name = "collab-persistence"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -890,7 +890,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -917,7 +917,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a462a49facbf3682717c3074b14fd29f19276e28#a462a49facbf3682717c3074b14fd29f19276e28"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",

View File

@ -109,11 +109,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "509
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" } collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }

View File

@ -112,11 +112,11 @@ impl EventIntegrationTest {
pub async fn create_field(&self, view_id: &str, field_type: FieldType) -> FieldPB { pub async fn create_field(&self, view_id: &str, field_type: FieldType) -> FieldPB {
EventBuilder::new(self.clone()) EventBuilder::new(self.clone())
.event(DatabaseEvent::CreateTypeOption) .event(DatabaseEvent::CreateField)
.payload(CreateFieldPayloadPB { .payload(CreateFieldPayloadPB {
view_id: view_id.to_string(), view_id: view_id.to_string(),
field_type, field_type,
type_option_data: None, ..Default::default()
}) })
.async_send() .async_send()
.await .await

View File

@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter};
use std::sync::Arc; use std::sync::Arc;
use collab_database::fields::Field; use collab_database::fields::Field;
use collab_database::views::FieldOrder; use collab_database::views::{FieldOrder, OrderObjectPosition};
use serde_repr::*; use serde_repr::*;
use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; use strum_macros::{EnumCount as EnumCountMacro, EnumIter};
@ -155,30 +155,94 @@ pub struct CreateFieldPayloadPB {
#[pb(index = 2)] #[pb(index = 2)]
pub field_type: FieldType, pub field_type: FieldType,
#[pb(index = 3, one_of)]
pub field_name: Option<String>,
/// If the type_option_data is not empty, it will be used to create the field. /// If the type_option_data is not empty, it will be used to create the field.
/// Otherwise, the default value will be used. /// Otherwise, the default value will be used.
#[pb(index = 3, one_of)] #[pb(index = 4, one_of)]
pub type_option_data: Option<Vec<u8>>, pub type_option_data: Option<Vec<u8>>,
#[pb(index = 5)]
pub field_position: CreateFieldPosition,
#[pb(index = 6, one_of)]
pub target_field_id: Option<String>,
}
#[derive(Debug, Default, ProtoBuf_Enum)]
#[repr(u8)]
pub enum CreateFieldPosition {
#[default]
End = 0,
Start = 1,
Before = 2,
After = 3,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct CreateFieldParams { pub struct CreateFieldParams {
pub view_id: String, pub view_id: String,
pub field_name: Option<String>,
pub field_type: FieldType, pub field_type: FieldType,
/// If the type_option_data is not empty, it will be used to create the field.
/// Otherwise, the default value will be used.
pub type_option_data: Option<Vec<u8>>, pub type_option_data: Option<Vec<u8>>,
pub position: OrderObjectPosition,
} }
impl TryInto<CreateFieldParams> for CreateFieldPayloadPB { impl TryInto<CreateFieldParams> for CreateFieldPayloadPB {
type Error = ErrorCode; type Error = ErrorCode;
fn try_into(self) -> Result<CreateFieldParams, Self::Error> { fn try_into(self) -> Result<CreateFieldParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
let field_name = match self.field_name {
Some(name) => Some(
NotEmptyStr::parse(name)
.map_err(|_| ErrorCode::InvalidParams)?
.0,
),
None => None,
};
let position = match &self.field_position {
CreateFieldPosition::Start => {
if self.target_field_id.is_some() {
return Err(ErrorCode::InvalidParams);
}
OrderObjectPosition::Start
},
CreateFieldPosition::End => {
if self.target_field_id.is_some() {
return Err(ErrorCode::InvalidParams);
}
OrderObjectPosition::End
},
CreateFieldPosition::Before => {
let field_id = self
.target_field_id
.ok_or_else(|| ErrorCode::InvalidParams)?;
let field_id = NotEmptyStr::parse(field_id)
.map_err(|_| ErrorCode::InvalidParams)?
.0;
OrderObjectPosition::Before(field_id)
},
CreateFieldPosition::After => {
let field_id = self
.target_field_id
.ok_or_else(|| ErrorCode::InvalidParams)?;
let field_id = NotEmptyStr::parse(field_id)
.map_err(|_| ErrorCode::InvalidParams)?
.0;
OrderObjectPosition::After(field_id)
},
};
Ok(CreateFieldParams { Ok(CreateFieldParams {
view_id: view_id.0, view_id: view_id.0,
field_name,
field_type: self.field_type, field_type: self.field_type,
type_option_data: self.type_option_data, type_option_data: self.type_option_data,
position,
}) })
} }
} }
@ -471,6 +535,7 @@ pub struct FieldChangesetParams {
/// it would be better to append it to the end of the list. /// it would be better to append it to the end of the list.
#[derive( #[derive(
Debug, Debug,
Copy,
Clone, Clone,
PartialEq, PartialEq,
Hash, Hash,

View File

@ -2,6 +2,7 @@ use std::sync::{Arc, Weak};
use collab_database::database::gen_row_id; use collab_database::database::gen_row_id;
use collab_database::rows::RowId; use collab_database::rows::RowId;
use collab_database::views::OrderObjectPosition;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
@ -319,16 +320,14 @@ pub(crate) async fn get_field_type_option_data_handler(
/// Create TypeOptionPB and save it. Return the FieldTypeOptionData. /// Create TypeOptionPB and save it. Return the FieldTypeOptionData.
#[tracing::instrument(level = "trace", skip(data, manager), err)] #[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn create_field_type_option_data_handler( pub(crate) async fn create_field_handler(
data: AFPluginData<CreateFieldPayloadPB>, data: AFPluginData<CreateFieldPayloadPB>,
manager: AFPluginState<Weak<DatabaseManager>>, manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<TypeOptionPB, FlowyError> { ) -> DataResult<TypeOptionPB, FlowyError> {
let manager = upgrade_manager(manager)?; let manager = upgrade_manager(manager)?;
let params: CreateFieldParams = data.into_inner().try_into()?; let params: CreateFieldParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?; let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
let (field, data) = database_editor let (field, data) = database_editor.create_field_with_type_option(&params).await;
.create_field_with_type_option(&params.view_id, &params.field_type, params.type_option_data)
.await;
let data = TypeOptionPB { let data = TypeOptionPB {
view_id: params.view_id, view_id: params.view_id,
@ -449,12 +448,16 @@ pub(crate) async fn create_row_handler(
CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), &fields).build(); CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), &fields).build();
let view_id = params.view_id; let view_id = params.view_id;
let group_id = params.group_id; let group_id = params.group_id;
let position = match params.start_row_id {
Some(row_id) => OrderObjectPosition::After(row_id.into()),
None => OrderObjectPosition::Start,
};
let params = collab_database::rows::CreateRowParams { let params = collab_database::rows::CreateRowParams {
id: gen_row_id(), id: gen_row_id(),
cells, cells,
height: 60, height: 60,
visibility: true, visibility: true,
prev_row_id: params.start_row_id, row_position: position,
timestamp: timestamp(), timestamp: timestamp(),
}; };
match database_editor match database_editor

View File

@ -31,7 +31,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::DuplicateField, duplicate_field_handler) .event(DatabaseEvent::DuplicateField, duplicate_field_handler)
.event(DatabaseEvent::MoveField, move_field_handler) .event(DatabaseEvent::MoveField, move_field_handler)
.event(DatabaseEvent::GetTypeOption, get_field_type_option_data_handler) .event(DatabaseEvent::GetTypeOption, get_field_type_option_data_handler)
.event(DatabaseEvent::CreateTypeOption, create_field_type_option_data_handler) .event(DatabaseEvent::CreateField, create_field_handler)
// Row // Row
.event(DatabaseEvent::CreateRow, create_row_handler) .event(DatabaseEvent::CreateRow, create_row_handler)
.event(DatabaseEvent::GetRow, get_row_handler) .event(DatabaseEvent::GetRow, get_row_handler)
@ -182,9 +182,10 @@ pub enum DatabaseEvent {
#[event(input = "TypeOptionPathPB", output = "TypeOptionPB")] #[event(input = "TypeOptionPathPB", output = "TypeOptionPB")]
GetTypeOption = 23, GetTypeOption = 23,
/// [CreateTypeOption] event is used to create a new FieldTypeOptionData. /// [CreateField] event is used to create a new field with an optional
/// TypeOptionData.
#[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")] #[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")]
CreateTypeOption = 24, CreateField = 24,
#[event(input = "DatabaseViewIdPB", output = "FieldPB")] #[event(input = "DatabaseViewIdPB", output = "FieldPB")]
GetPrimaryField = 25, GetPrimaryField = 25,

View File

@ -5,7 +5,7 @@ use bytes::Bytes;
use collab_database::database::MutexDatabase; use collab_database::database::MutexDatabase;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId}; use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId};
use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, OrderObjectPosition};
use futures::StreamExt; use futures::StreamExt;
use tokio::sync::{broadcast, RwLock}; use tokio::sync::{broadcast, RwLock};
use tracing::{event, warn}; use tracing::{event, warn};
@ -470,25 +470,26 @@ impl DatabaseEditor {
}) })
} }
pub async fn create_field_with_type_option( pub async fn create_field_with_type_option(&self, params: &CreateFieldParams) -> (Field, Bytes) {
&self, let name = params
view_id: &str, .field_name
field_type: &FieldType, .clone()
type_option_data: Option<Vec<u8>>, .unwrap_or_else(|| params.field_type.default_name());
) -> (Field, Bytes) { let type_option_data = match &params.type_option_data {
let name = field_type.default_name(); None => default_type_option_data_from_type(&params.field_type),
let type_option_data = match type_option_data { Some(type_option_data) => {
None => default_type_option_data_from_type(field_type), type_option_data_from_pb_or_default(type_option_data.clone(), &params.field_type)
Some(type_option_data) => type_option_data_from_pb_or_default(type_option_data, field_type), },
}; };
let (index, field) = self.database.lock().create_field_with_mut( let (index, field) = self.database.lock().create_field_with_mut(
view_id, &params.view_id,
name, name,
field_type.into(), params.field_type.into(),
&params.position,
|field| { |field| {
field field
.type_options .type_options
.insert(field_type.to_string(), type_option_data.clone()); .insert(params.field_type.to_string(), type_option_data.clone());
}, },
default_field_settings_by_layout_map(), default_field_settings_by_layout_map(),
); );
@ -497,7 +498,10 @@ impl DatabaseEditor {
.notify_did_insert_database_field(field.clone(), index) .notify_did_insert_database_field(field.clone(), index)
.await; .await;
(field, type_option_to_pb(type_option_data, field_type)) (
field,
type_option_to_pb(type_option_data, &params.field_type),
)
} }
pub async fn move_field( pub async fn move_field(
@ -1223,6 +1227,7 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
view_id, view_id,
name.to_string(), name.to_string(),
field_type.clone().into(), field_type.clone().into(),
&OrderObjectPosition::default(),
|field| { |field| {
field field
.type_options .type_options

View File

@ -1,6 +1,6 @@
use collab_database::database::{gen_field_id, MutexDatabase}; use collab_database::database::{gen_field_id, MutexDatabase};
use collab_database::fields::Field; use collab_database::fields::Field;
use collab_database::views::{DatabaseLayout, LayoutSetting}; use collab_database::views::{DatabaseLayout, LayoutSetting, OrderObjectPosition};
use std::sync::Arc; use std::sync::Arc;
use crate::entities::FieldType; use crate::entities::FieldType;
@ -87,10 +87,12 @@ impl DatabaseLayoutDepsResolver {
tracing::trace!("Create a new date field after layout type change"); tracing::trace!("Create a new date field after layout type change");
let field = self.create_date_field(); let field = self.create_date_field();
let field_id = field.id.clone(); let field_id = field.id.clone();
self self.database.lock().create_field(
.database None,
.lock() field,
.create_field(field, default_field_settings_by_layout_map()); &OrderObjectPosition::End,
default_field_settings_by_layout_map(),
);
field_id field_id
}, },
Some(date_field) => date_field.id, Some(date_field) => date_field.id,

View File

@ -4,6 +4,7 @@ use std::sync::Arc;
use collab_database::database::{gen_database_view_id, timestamp}; use collab_database::database::{gen_database_view_id, timestamp};
use collab_database::fields::Field; use collab_database::fields::Field;
use collab_database::rows::{CreateRowParams, RowDetail, RowId}; use collab_database::rows::{CreateRowParams, RowDetail, RowId};
use collab_database::views::OrderObjectPosition;
use strum::EnumCount; use strum::EnumCount;
use event_integration::folder_event::ViewTest; use event_integration::folder_event::ViewTest;
@ -410,7 +411,7 @@ impl<'a> TestRowBuilder<'a> {
cells: self.cell_build.build(), cells: self.cell_build.build(),
height: 60, height: 60,
visibility: true, visibility: true,
prev_row_id: None, row_position: OrderObjectPosition::End,
timestamp: timestamp(), timestamp: timestamp(),
} }
} }

View File

@ -64,10 +64,7 @@ impl DatabaseFieldTest {
match script { match script {
FieldScript::CreateField { params } => { FieldScript::CreateField { params } => {
self.field_count += 1; self.field_count += 1;
self self.editor.create_field_with_type_option(&params).await;
.editor
.create_field_with_type_option(&self.view_id, &params.field_type, params.type_option_data)
.await;
let fields = self.editor.get_fields(&self.view_id, None); let fields = self.editor.get_fields(&self.view_id, None);
assert_eq!(self.field_count, fields.len()); assert_eq!(self.field_count, fields.len());
}, },

View File

@ -1,4 +1,5 @@
use collab_database::fields::Field; use collab_database::fields::Field;
use collab_database::views::OrderObjectPosition;
use flowy_database2::entities::{CreateFieldParams, FieldType}; use flowy_database2::entities::{CreateFieldParams, FieldType};
use flowy_database2::services::field::{ use flowy_database2::services::field::{
type_option_to_pb, DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, type_option_to_pb, DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder,
@ -19,6 +20,8 @@ pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) {
view_id: grid_id.to_owned(), view_id: grid_id.to_owned(),
field_type, field_type,
type_option_data: Some(type_option_data), type_option_data: Some(type_option_data),
field_name: None,
position: OrderObjectPosition::default(),
}; };
(params, text_field) (params, text_field)
} }
@ -38,6 +41,8 @@ pub fn create_single_select_field(grid_id: &str) -> (CreateFieldParams, Field) {
view_id: grid_id.to_owned(), view_id: grid_id.to_owned(),
field_type, field_type,
type_option_data: Some(type_option_data), type_option_data: Some(type_option_data),
field_name: None,
position: OrderObjectPosition::default(),
}; };
(params, single_select_field) (params, single_select_field)
} }
@ -60,6 +65,8 @@ pub fn create_date_field(grid_id: &str) -> (CreateFieldParams, Field) {
view_id: grid_id.to_owned(), view_id: grid_id.to_owned(),
field_type: FieldType::DateTime, field_type: FieldType::DateTime,
type_option_data: Some(type_option_data), type_option_data: Some(type_option_data),
field_name: None,
position: OrderObjectPosition::default(),
}; };
(params, field) (params, field)
} }
@ -92,6 +99,8 @@ pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFi
view_id: grid_id.to_owned(), view_id: grid_id.to_owned(),
field_type, field_type,
type_option_data: Some(type_option_data), type_option_data: Some(type_option_data),
field_name: None,
position: OrderObjectPosition::default(),
}; };
(params, field) (params, field)
} }

View File

@ -6,6 +6,7 @@ use chrono::{offset, Duration};
use collab_database::database::gen_row_id; use collab_database::database::gen_row_id;
use collab_database::rows::CreateRowParams; use collab_database::rows::CreateRowParams;
use collab_database::views::OrderObjectPosition;
use flowy_database2::entities::FieldType; use flowy_database2::entities::FieldType;
use flowy_database2::services::cell::CellBuilder; use flowy_database2::services::cell::CellBuilder;
use flowy_database2::services::field::DateCellData; use flowy_database2::services::field::DateCellData;
@ -34,7 +35,7 @@ async fn group_by_date_test() {
cells, cells,
height: 60, height: 60,
visibility: true, visibility: true,
prev_row_id: None, row_position: OrderObjectPosition::default(),
timestamp: 0, timestamp: 0,
}; };
let res = test.editor.create_row(&test.view_id, None, params).await; let res = test.editor.create_row(&test.view_id, None, params).await;