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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 478 additions and 256 deletions

View File

@ -10,8 +10,8 @@ import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid cell', () {
testWidgets('edit text cell', (tester) async {
group('edit grid cell:', () {
testWidgets('text', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -34,7 +34,7 @@ void main() {
// Make sure the text cells are filled with the right content when there are
// multiple text cell
testWidgets('edit multiple text cells', (tester) async {
testWidgets('multiple text cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(
@ -73,7 +73,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit number cell', (tester) async {
testWidgets('number', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -131,7 +131,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit checkbox cell', (tester) async {
testWidgets('checkbox', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -149,7 +149,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit create time cell', (tester) async {
testWidgets('created time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -167,7 +167,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit last time cell', (tester) async {
testWidgets('last modified time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -185,7 +185,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit date time cell', (tester) async {
testWidgets('date time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -275,7 +275,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit single select cell', (tester) async {
testWidgets('single select', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -347,7 +347,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit multi select cell', (tester) async {
testWidgets('multi select', (tester) async {
final tags = [
'tag 1',
'tag 2',
@ -436,107 +436,107 @@ void main() {
await tester.pumpAndSettle();
});
});
testWidgets('edit checklist cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
testWidgets('checklist', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.Checklist;
await tester.createField(fieldType, fieldType.name);
const fieldType = FieldType.Checklist;
await tester.createField(fieldType, fieldType.name);
// assert that there is no progress bar in the grid
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
// assert that there is no progress bar in the grid
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
// tap on the first checklist cell
await tester.tapChecklistCellInGrid(rowIndex: 0);
// tap on the first checklist cell
await tester.tapChecklistCellInGrid(rowIndex: 0);
// assert that the checklist editor is shown
tester.assertChecklistEditorVisible(visible: true);
// assert that the checklist editor is shown
tester.assertChecklistEditorVisible(visible: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
// assert that the task is displayed
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 0",
isChecked: false,
);
// assert that the task is displayed
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 0",
isChecked: false,
);
// update the task's name
await tester.renameChecklistTask(index: 0, name: "task 1");
// update the task's name
await tester.renameChecklistTask(index: 0, name: "task 1");
// assert that the task's name is updated
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
// assert that the task's name is updated
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
// dismiss new task editor
await tester.dismissCellEditor();
// dismiss new task editor
await tester.dismissCellEditor();
// dismiss checklist cell editor
await tester.dismissCellEditor();
// dismiss checklist cell editor
await tester.dismissCellEditor();
// assert that progress bar is shown in grid at 0%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
// assert that progress bar is shown in grid at 0%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
// start editing the first checklist cell again
await tester.tapChecklistCellInGrid(rowIndex: 0);
// start editing the first checklist cell again
await tester.tapChecklistCellInGrid(rowIndex: 0);
// create another task with the create button
await tester.createNewChecklistTask(name: "task 2", button: true);
// create another task with the create button
await tester.createNewChecklistTask(name: "task 2", button: true);
// assert that the task was inserted
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
// assert that the task was inserted
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
// mark it as complete
await tester.checkChecklistTask(index: 1);
// mark it as complete
await tester.checkChecklistTask(index: 1);
// assert that the task was checked in the editor
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: true,
);
// assert that the task was checked in the editor
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: true,
);
// dismiss checklist editor
await tester.dismissCellEditor();
await tester.dismissCellEditor();
// dismiss checklist editor
await tester.dismissCellEditor();
await tester.dismissCellEditor();
// assert that progressbar is shown in grid at 50%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
// assert that progressbar is shown in grid at 50%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
// re-open the cell editor
await tester.tapChecklistCellInGrid(rowIndex: 0);
// re-open the cell editor
await tester.tapChecklistCellInGrid(rowIndex: 0);
// hover over first task and delete it
await tester.deleteChecklistTask(index: 0);
// hover over first task and delete it
await tester.deleteChecklistTask(index: 0);
// dismiss cell editor
await tester.dismissCellEditor();
// dismiss cell editor
await tester.dismissCellEditor();
// assert that progressbar is shown in grid at 100%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
// assert that progressbar is shown in grid at 100%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
// re-open the cell edior
await tester.tapChecklistCellInGrid(rowIndex: 0);
// re-open the cell edior
await tester.tapChecklistCellInGrid(rowIndex: 0);
// delete the remaining task
await tester.deleteChecklistTask(index: 0);
// delete the remaining task
await tester.deleteChecklistTask(index: 0);
// dismiss the cell editor
await tester.dismissCellEditor();
// dismiss the cell editor
await tester.dismissCellEditor();
// check that the progress bar is not viisble
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
// check that the progress bar is not viisble
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
});
});
}

View File

@ -12,7 +12,7 @@ import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid page', () {
group('grid field editor:', () {
testWidgets('rename existing field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -98,7 +98,7 @@ void main() {
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// Delete the field
// duplicate the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapDuplicatePropertyButton();
@ -106,6 +106,29 @@ void main() {
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 {
await tester.initializeAppFlowy();
await tester.tapGoButton();

View File

@ -765,6 +765,20 @@ extension AppFlowyDatabaseTest on WidgetTester {
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.
Future<void> tapHidePropertyButton() async {
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_backend/log.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 FieldSettingsBackendService fieldSettingsService;
final TypeOptionController typeOptionController;
final void Function(String newFieldId)? onFieldInserted;
FieldEditorBloc({
required this.viewId,
required this.field,
required this.fieldController,
this.onFieldInserted,
required FieldTypeOptionLoader loader,
}) : typeOptionController = TypeOptionController(
field: field,
@ -73,6 +76,28 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
final result = await fieldService.updateField(name: newName);
_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 {
final currentVisibility =
state.field.visibility ?? FieldVisibility.AlwaysShown;
@ -122,6 +147,8 @@ class FieldEditorEvent with _$FieldEditorEvent {
const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) =
_SwitchFieldType;
const factory FieldEditorEvent.renameField(final String name) = _RenameField;
const factory FieldEditorEvent.insertLeft() = _InsertLeft;
const factory FieldEditorEvent.insertRight() = _InsertRight;
const factory FieldEditorEvent.toggleFieldVisibility() =
_ToggleFieldVisiblity;
const factory FieldEditorEvent.deleteField() = _DeleteField;

View File

@ -26,11 +26,20 @@ class TypeOptionBackendService {
static Future<Either<TypeOptionPB, FlowyError>> createFieldTypeOption({
required String viewId,
FieldType fieldType = FieldType.RichText,
CreateFieldPosition position = CreateFieldPosition.End,
String? targetFieldId,
}) {
final payload = CreateFieldPayloadPB.create()
..viewId = viewId
..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()) {
on<GridHeaderEvent>(
(event, emit) async {
await event.map(
initial: (_InitialHeader value) {
await event.when(
initial: () {
_startListening();
add(
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
);
},
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
didReceiveFieldUpdate: (List<FieldInfo> fields) {
emit(
state.copyWith(
fields: value.fields
fields: fields
.where(
(element) =>
element.visibility != null &&
@ -40,8 +40,17 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
),
);
},
moveField: (_MoveField value) async {
await _moveField(value, emit);
startEditingField: (fieldId) {
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(
_MoveField value,
FieldPB field,
int fromIndex,
int toIndex,
Emitter<GridHeaderState> emit,
) async {
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));
final fieldService =
FieldBackendService(viewId: viewId, fieldId: value.field.id);
final result = await fieldService.moveField(
value.fromIndex,
value.toIndex,
);
final fieldService = FieldBackendService(viewId: viewId, fieldId: field.id);
final result = await fieldService.moveField(fromIndex, toIndex);
result.fold((l) {}, (err) => Log.error(err));
}
@ -79,6 +86,11 @@ class GridHeaderEvent with _$GridHeaderEvent {
const factory GridHeaderEvent.initial() = _InitialHeader;
const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =
_DidReceiveFieldUpdate;
const factory GridHeaderEvent.startEditingField(String fieldId) =
_StartEditingField;
const factory GridHeaderEvent.startEditingNewField(String fieldId) =
_StartEditingNewField;
const factory GridHeaderEvent.endEditingField() = _EndEditingField;
const factory GridHeaderEvent.moveField(
FieldPB field,
int fromIndex,
@ -88,8 +100,12 @@ class GridHeaderEvent with _$GridHeaderEvent {
@freezed
class GridHeaderState with _$GridHeaderState {
const factory GridHeaderState({required List<FieldInfo> fields}) =
_GridHeaderState;
const factory 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';
class GridFieldCell extends StatefulWidget {
final String viewId;
final FieldController fieldController;
final FieldInfo fieldInfo;
const GridFieldCell({
super.key,
required this.viewId,
required this.fieldController,
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
State<GridFieldCell> createState() => _GridFieldCellState();
}
@ -39,6 +50,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
super.initState();
popoverController = PopoverController();
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
if (widget.isEditing) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
popoverController.show();
});
}
}
@override
@ -46,6 +62,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) {
_bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo));
}
if (widget.isEditing) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
popoverController.show();
});
}
super.didUpdateWidget(oldWidget);
}
@ -62,16 +83,20 @@ class _GridFieldCellState extends State<GridFieldCell> {
direction: PopoverDirection.bottomWithLeftAligned,
controller: popoverController,
popupBuilder: (BuildContext context) {
widget.onEditorOpened();
return FieldEditor(
viewId: widget.viewId,
fieldController: widget.fieldController,
field: widget.fieldInfo.field,
initialPage: FieldEditorPage.general,
initialPage: widget.isNew
? FieldEditorPage.details
: FieldEditorPage.general,
onFieldInserted: widget.onFieldInsertedOnEitherSide,
);
},
child: FieldCellButton(
field: widget.fieldInfo.field,
onTap: () => popoverController.show(),
onTap: widget.onTap,
),
);

View File

@ -30,6 +30,7 @@ class FieldEditor extends StatefulWidget {
final FieldController fieldController;
final FieldPB field;
final FieldEditorPage initialPage;
final void Function(String fieldId)? onFieldInserted;
const FieldEditor({
super.key,
@ -37,6 +38,7 @@ class FieldEditor extends StatefulWidget {
required this.field,
required this.fieldController,
this.initialPage = FieldEditorPage.details,
this.onFieldInserted,
});
@override
@ -61,6 +63,7 @@ class _FieldEditorState extends State<FieldEditor> {
viewId: widget.viewId,
field: widget.field,
fieldController: widget.fieldController,
onFieldInserted: widget.onFieldInserted,
loader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: widget.field,
@ -89,6 +92,10 @@ class _FieldEditorState extends State<FieldEditor> {
},
),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.insertLeft),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.insertRight),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.toggleVisibility),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.duplicate),
@ -170,40 +177,56 @@ class FieldActionCell extends StatelessWidget {
),
onHover: (_) => popoverMutex?.close(),
onTap: () => action.run(context, viewId, fieldInfo),
leftIcon: FlowySvg(
action.icon(fieldInfo),
size: const Size.square(16),
color: enable ? null : Theme.of(context).disabledColor,
leftIcon: action.icon(
fieldInfo,
enable ? null : Theme.of(context).disabledColor,
),
);
}
}
enum FieldAction {
insertLeft,
insertRight,
toggleVisibility,
duplicate,
delete,
}
delete;
extension _FieldActionExtension on FieldAction {
FlowySvgData icon(FieldInfo fieldInfo) {
Widget icon(FieldInfo fieldInfo, Color? color) {
late final FlowySvgData svgData;
switch (this) {
case FieldAction.insertLeft:
svgData = FlowySvgs.arrow_s;
case FieldAction.insertRight:
svgData = FlowySvgs.arrow_s;
case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
return FlowySvgs.show_m;
svgData = FlowySvgs.show_m;
} else {
return FlowySvgs.hide_s;
svgData = FlowySvgs.hide_s;
}
case FieldAction.duplicate:
return FlowySvgs.copy_s;
svgData = FlowySvgs.copy_s;
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) {
switch (this) {
case FieldAction.insertLeft:
return LocaleKeys.grid_field_insertLeft.tr();
case FieldAction.insertRight:
return LocaleKeys.grid_field_insertRight.tr();
case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
@ -220,6 +243,18 @@ extension _FieldActionExtension on FieldAction {
void run(BuildContext context, String viewId, FieldInfo fieldInfo) {
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:
PopoverContainer.of(context).close();
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_editor/appflowy_editor.dart' hide Log;
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_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reorderables/reorderables.dart';
import '../../../../application/field/type_option/type_option_service.dart';
import '../../layout/sizes.dart';
import 'field_editor.dart';
import 'field_cell.dart';
class GridHeaderSliverAdaptor extends StatefulWidget {
@ -93,7 +90,6 @@ class _GridHeaderState extends State<_GridHeader> {
@override
Widget build(BuildContext context) {
return BlocBuilder<GridHeaderBloc, GridHeaderState>(
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final cells = state.fields
.map(
@ -103,6 +99,17 @@ class _GridHeaderState extends State<_GridHeader> {
viewId: widget.viewId,
fieldInfo: fieldInfo,
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(
key: _getKeyById(fieldInfo.id),
@ -184,74 +191,57 @@ class _CellTrailing extends StatelessWidget {
)
: null,
padding: GridSize.headerContentInsets,
child: CreateFieldButton(viewId: viewId),
child: CreateFieldButton(
viewId: viewId,
onFieldCreated: (fieldId) => context
.read<GridHeaderBloc>()
.add(GridHeaderEvent.startEditingNewField(fieldId)),
),
);
}
}
class CreateFieldButton extends StatefulWidget {
final String viewId;
const CreateFieldButton({
super.key,
required this.viewId,
required this.onFieldCreated,
});
final String viewId;
final void Function(String fieldId) onFieldCreated;
@override
State<CreateFieldButton> createState() => _CreateFieldButtonState();
}
class _CreateFieldButtonState extends State<CreateFieldButton> {
final popoverController = PopoverController();
late TypeOptionPB typeOption;
@override
Widget build(BuildContext context) {
final fieldController =
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
? GridSize.cellContentInsets
: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
radius: BorderRadius.zero,
text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(),
overflow: TextOverflow.ellipsis,
color:
PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
),
hoverColor: AFThemeExtension.of(context).greyHover,
onTap: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: widget.viewId,
);
result.fold(
(l) {
typeOption = l;
popoverController.show();
},
(r) => Log.error("Failed to create field type option: $r"),
);
},
leftIcon: FlowySvg(
FlowySvgs.add_s,
color:
PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
),
return FlowyButton(
margin: PlatformExtension.isDesktop
? GridSize.cellContentInsets
: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
radius: BorderRadius.zero,
text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(),
overflow: TextOverflow.ellipsis,
color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
),
popupBuilder: (BuildContext popoverContext) {
return FieldEditor(
hoverColor: AFThemeExtension.of(context).greyHover,
onTap: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: widget.viewId,
fieldController: fieldController,
field: typeOption.field_2,
);
result.fold(
(typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id),
(err) => Log.error("Failed to create field type option: $err"),
);
},
leftIcon: FlowySvg(
FlowySvgs.add_s,
color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
),
);
}
}

View File

@ -880,7 +880,7 @@ dependencies = [
[[package]]
name = "collab"
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 = [
"anyhow",
"async-trait",
@ -900,7 +900,7 @@ dependencies = [
[[package]]
name = "collab-database"
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 = [
"anyhow",
"async-trait",
@ -930,7 +930,7 @@ dependencies = [
[[package]]
name = "collab-derive"
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 = [
"proc-macro2",
"quote",
@ -942,7 +942,7 @@ dependencies = [
[[package]]
name = "collab-document"
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 = [
"anyhow",
"collab",
@ -962,7 +962,7 @@ dependencies = [
[[package]]
name = "collab-entity"
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 = [
"anyhow",
"bytes",
@ -976,7 +976,7 @@ dependencies = [
[[package]]
name = "collab-folder"
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 = [
"anyhow",
"chrono",
@ -1018,7 +1018,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
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 = [
"anyhow",
"async-trait",
@ -1040,7 +1040,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
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 = [
"anyhow",
"async-trait",
@ -1067,7 +1067,7 @@ dependencies = [
[[package]]
name = "collab-user"
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 = [
"anyhow",
"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:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-persistence = { 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 = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }

View File

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

View File

@ -1,6 +1,6 @@
import { CreateFieldPayloadPB, FieldType, TypeOptionPathPB, UpdateFieldTypePayloadPB } from '@/services/backend';
import {
DatabaseEventCreateTypeOption,
DatabaseEventCreateField,
DatabaseEventGetTypeOption,
DatabaseEventUpdateFieldType,
} from '@/services/backend/events/flowy-database2';
@ -11,7 +11,7 @@ export class TypeOptionBackendService {
createTypeOption = (fieldType: FieldType) => {
const payload = CreateFieldPayloadPB.fromObject({ view_id: this.viewId, field_type: fieldType });
return DatabaseEventCreateTypeOption(payload);
return DatabaseEventCreateField(payload);
};
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]]
name = "collab"
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 = [
"anyhow",
"async-trait",
@ -750,7 +750,7 @@ dependencies = [
[[package]]
name = "collab-database"
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 = [
"anyhow",
"async-trait",
@ -780,7 +780,7 @@ dependencies = [
[[package]]
name = "collab-derive"
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 = [
"proc-macro2",
"quote",
@ -792,7 +792,7 @@ dependencies = [
[[package]]
name = "collab-document"
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 = [
"anyhow",
"collab",
@ -812,7 +812,7 @@ dependencies = [
[[package]]
name = "collab-entity"
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 = [
"anyhow",
"bytes",
@ -826,7 +826,7 @@ dependencies = [
[[package]]
name = "collab-folder"
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 = [
"anyhow",
"chrono",
@ -868,7 +868,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
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 = [
"anyhow",
"async-trait",
@ -890,7 +890,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
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 = [
"anyhow",
"async-trait",
@ -917,7 +917,7 @@ dependencies = [
[[package]]
name = "collab-user"
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 = [
"anyhow",
"collab",

View File

@ -67,7 +67,7 @@ futures = "0.3.29"
tokio = "1.34.0"
tokio-stream = "0.1.14"
async-trait = "0.1.74"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
lru = "0.12.0"
[profile.dev]
@ -109,11 +109,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "509
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-persistence = { 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 = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a462a49facbf3682717c3074b14fd29f19276e28" }
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 {
EventBuilder::new(self.clone())
.event(DatabaseEvent::CreateTypeOption)
.event(DatabaseEvent::CreateField)
.payload(CreateFieldPayloadPB {
view_id: view_id.to_string(),
field_type,
type_option_data: None,
..Default::default()
})
.async_send()
.await

View File

@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter};
use std::sync::Arc;
use collab_database::fields::Field;
use collab_database::views::FieldOrder;
use collab_database::views::{FieldOrder, OrderObjectPosition};
use serde_repr::*;
use strum_macros::{EnumCount as EnumCountMacro, EnumIter};
@ -155,30 +155,94 @@ pub struct CreateFieldPayloadPB {
#[pb(index = 2)]
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.
/// Otherwise, the default value will be used.
#[pb(index = 3, one_of)]
#[pb(index = 4, one_of)]
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)]
pub struct CreateFieldParams {
pub view_id: String,
pub field_name: Option<String>,
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 position: OrderObjectPosition,
}
impl TryInto<CreateFieldParams> for CreateFieldPayloadPB {
type Error = ErrorCode;
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 {
view_id: view_id.0,
field_name,
field_type: self.field_type,
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.
#[derive(
Debug,
Copy,
Clone,
PartialEq,
Hash,

View File

@ -2,6 +2,7 @@ use std::sync::{Arc, Weak};
use collab_database::database::gen_row_id;
use collab_database::rows::RowId;
use collab_database::views::OrderObjectPosition;
use tokio::sync::oneshot;
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.
#[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>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<TypeOptionPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let params: CreateFieldParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
let (field, data) = database_editor
.create_field_with_type_option(&params.view_id, &params.field_type, params.type_option_data)
.await;
let (field, data) = database_editor.create_field_with_type_option(&params).await;
let data = TypeOptionPB {
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();
let view_id = params.view_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 {
id: gen_row_id(),
cells,
height: 60,
visibility: true,
prev_row_id: params.start_row_id,
row_position: position,
timestamp: timestamp(),
};
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::MoveField, move_field_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
.event(DatabaseEvent::CreateRow, create_row_handler)
.event(DatabaseEvent::GetRow, get_row_handler)
@ -182,9 +182,10 @@ pub enum DatabaseEvent {
#[event(input = "TypeOptionPathPB", output = "TypeOptionPB")]
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")]
CreateTypeOption = 24,
CreateField = 24,
#[event(input = "DatabaseViewIdPB", output = "FieldPB")]
GetPrimaryField = 25,

View File

@ -5,7 +5,7 @@ use bytes::Bytes;
use collab_database::database::MutexDatabase;
use collab_database::fields::{Field, TypeOptionData};
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 tokio::sync::{broadcast, RwLock};
use tracing::{event, warn};
@ -470,25 +470,26 @@ impl DatabaseEditor {
})
}
pub async fn create_field_with_type_option(
&self,
view_id: &str,
field_type: &FieldType,
type_option_data: Option<Vec<u8>>,
) -> (Field, Bytes) {
let name = field_type.default_name();
let type_option_data = match type_option_data {
None => default_type_option_data_from_type(field_type),
Some(type_option_data) => type_option_data_from_pb_or_default(type_option_data, field_type),
pub async fn create_field_with_type_option(&self, params: &CreateFieldParams) -> (Field, Bytes) {
let name = params
.field_name
.clone()
.unwrap_or_else(|| params.field_type.default_name());
let type_option_data = match &params.type_option_data {
None => default_type_option_data_from_type(&params.field_type),
Some(type_option_data) => {
type_option_data_from_pb_or_default(type_option_data.clone(), &params.field_type)
},
};
let (index, field) = self.database.lock().create_field_with_mut(
view_id,
&params.view_id,
name,
field_type.into(),
params.field_type.into(),
&params.position,
|field| {
field
.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(),
);
@ -497,7 +498,10 @@ impl DatabaseEditor {
.notify_did_insert_database_field(field.clone(), index)
.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(
@ -1223,6 +1227,7 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
view_id,
name.to_string(),
field_type.clone().into(),
&OrderObjectPosition::default(),
|field| {
field
.type_options

View File

@ -1,6 +1,6 @@
use collab_database::database::{gen_field_id, MutexDatabase};
use collab_database::fields::Field;
use collab_database::views::{DatabaseLayout, LayoutSetting};
use collab_database::views::{DatabaseLayout, LayoutSetting, OrderObjectPosition};
use std::sync::Arc;
use crate::entities::FieldType;
@ -87,10 +87,12 @@ impl DatabaseLayoutDepsResolver {
tracing::trace!("Create a new date field after layout type change");
let field = self.create_date_field();
let field_id = field.id.clone();
self
.database
.lock()
.create_field(field, default_field_settings_by_layout_map());
self.database.lock().create_field(
None,
field,
&OrderObjectPosition::End,
default_field_settings_by_layout_map(),
);
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::fields::Field;
use collab_database::rows::{CreateRowParams, RowDetail, RowId};
use collab_database::views::OrderObjectPosition;
use strum::EnumCount;
use event_integration::folder_event::ViewTest;
@ -410,7 +411,7 @@ impl<'a> TestRowBuilder<'a> {
cells: self.cell_build.build(),
height: 60,
visibility: true,
prev_row_id: None,
row_position: OrderObjectPosition::End,
timestamp: timestamp(),
}
}

View File

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

View File

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

View File

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