From bb414c3fd6b97fe19d5442b782851fc508f632e3 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Fri, 15 Mar 2024 22:58:55 +0800 Subject: [PATCH] chore: enable relation to (#4866) * chore: enable relation to * chore: fix database name and improve UI * chore: remove database view id from relation type option * chore: add remove row id test * chore: improve appearance of untitled rows * chore: empty in row detail * fix: cannot add events after closing --------- Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com> --- .../cell/bloc/relation_cell_bloc.dart | 104 +++++++--- .../relation_type_option_cubit.dart | 63 ++++++ .../database/domain/database_service.dart | 2 +- .../widgets/header/field_type_list.dart | 1 + .../widgets/header/type_option/relation.dart | 187 +++++++++--------- .../relation_card_cell.dart | 21 +- .../desktop_grid_relation_cell.dart | 28 +-- .../desktop_row_detail_relation_cell.dart | 58 +++--- .../cell_editor/relation_cell_editor.dart | 114 +++++++---- frontend/resources/translations/en.json | 2 + .../tests/database/local_test/test.rs | 24 +++ .../src/entities/database_entities.rs | 14 +- .../flowy-database2/src/event_handler.rs | 18 +- .../rust-lib/flowy-database2/src/manager.rs | 34 ++-- .../flowy-folder/src/event_handler.rs | 2 +- .../rust-lib/flowy-folder/src/event_map.rs | 2 +- .../src/anon_user/migrate_anon_user_collab.rs | 4 +- .../anon_user/sync_supabase_user_collab.rs | 6 +- .../data_import/appflowy_data_import.rs | 6 +- 19 files changed, 459 insertions(+), 231 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart index 2e56e1691a..39528a97c2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart @@ -1,10 +1,14 @@ import 'dart:async'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; +import 'package:appflowy/plugins/database/domain/field_service.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -35,12 +39,14 @@ class RelationCellBloc extends Bloc { (event, emit) async { await event.when( didUpdateCell: (RelationCellDataPB? cellData) async { - if (cellData == null || cellData.rowIds.isEmpty) { + if (cellData == null || + cellData.rowIds.isEmpty || + state.relatedDatabaseMeta == null) { emit(state.copyWith(rows: const [])); return; } final payload = RepeatedRowIdPB( - databaseId: state.relatedDatabaseId, + databaseId: state.relatedDatabaseMeta!.databaseId, rowIds: cellData.rowIds, ); final result = @@ -54,8 +60,16 @@ class RelationCellBloc extends Bloc { ); emit(state.copyWith(rows: rows)); }, - didUpdateRelationDatabaseId: (databaseId) { - emit(state.copyWith(relatedDatabaseId: databaseId)); + didUpdateRelationTypeOption: (typeOption) async { + if (typeOption.databaseId.isEmpty) { + return; + } + final meta = await _loadDatabaseMeta(typeOption.databaseId); + emit(state.copyWith(relatedDatabaseMeta: meta)); + _loadCellData(); + }, + selectDatabaseId: (databaseId) async { + await _updateTypeOption(databaseId); }, selectRow: (rowId) async { await _handleSelectRow(rowId); @@ -73,29 +87,30 @@ class RelationCellBloc extends Bloc { } }, onCellFieldChanged: (field) { - if (!isClosed) { - // hack: SingleFieldListener receives notification before - // FieldController's copy is updated. - Future.delayed(const Duration(milliseconds: 50), () { + // hack: SingleFieldListener receives notification before + // FieldController's copy is updated. + Future.delayed(const Duration(milliseconds: 50), () { + if (!isClosed) { final RelationTypeOptionPB typeOption = cellController.getTypeOption(RelationTypeOptionDataParser()); - add( - RelationCellEvent.didUpdateRelationDatabaseId( - typeOption.databaseId, - ), - ); - }); - } + add(RelationCellEvent.didUpdateRelationTypeOption(typeOption)); + } + }); }, ); } void _init() { - final RelationTypeOptionPB typeOption = + final typeOption = cellController.getTypeOption(RelationTypeOptionDataParser()); - add(RelationCellEvent.didUpdateRelationDatabaseId(typeOption.databaseId)); + add(RelationCellEvent.didUpdateRelationTypeOption(typeOption)); + } + + void _loadCellData() { final cellData = cellController.getCellData(); - add(RelationCellEvent.didUpdateCell(cellData)); + if (!isClosed) { + add(RelationCellEvent.didUpdateCell(cellData)); + } } Future _handleSelectRow(String rowId) async { @@ -115,25 +130,66 @@ class RelationCellBloc extends Bloc { final result = await DatabaseEventUpdateRelationCell(payload).send(); result.fold((l) => null, (err) => Log.error(err)); } + + Future _loadDatabaseMeta(String databaseId) async { + final getDatabaseResult = await DatabaseEventGetDatabases().send(); + final databaseMeta = getDatabaseResult.fold( + (s) => s.items.firstWhereOrNull( + (metaPB) => metaPB.databaseId == databaseId, + ), + (f) => null, + ); + if (databaseMeta != null) { + final result = + await ViewBackendService.getView(databaseMeta.inlineViewId); + return result.fold( + (s) => DatabaseMeta( + databaseId: databaseId, + inlineViewId: databaseMeta.inlineViewId, + databaseName: s.name, + ), + (f) => null, + ); + } + return null; + } + + Future _updateTypeOption(String databaseId) async { + final newDateTypeOption = RelationTypeOptionPB( + databaseId: databaseId, + ); + + final result = await FieldBackendService.updateFieldTypeOption( + viewId: cellController.viewId, + fieldId: cellController.fieldInfo.id, + typeOptionData: newDateTypeOption.writeToBuffer(), + ); + result.fold((s) => null, (err) => Log.error(err)); + } } @freezed class RelationCellEvent with _$RelationCellEvent { - const factory RelationCellEvent.didUpdateRelationDatabaseId( - String databaseId, - ) = _DidUpdateRelationDatabaseId; + const factory RelationCellEvent.didUpdateRelationTypeOption( + RelationTypeOptionPB typeOption, + ) = _DidUpdateRelationTypeOption; const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) = _DidUpdateCell; + const factory RelationCellEvent.selectDatabaseId( + String databaseId, + ) = _SelectDatabaseId; const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId; } @freezed class RelationCellState with _$RelationCellState { const factory RelationCellState({ - required String relatedDatabaseId, + required DatabaseMeta? relatedDatabaseMeta, required List rows, }) = _RelationCellState; - factory RelationCellState.initial() => - const RelationCellState(relatedDatabaseId: "", rows: []); + factory RelationCellState.initial() => const RelationCellState( + relatedDatabaseMeta: null, + rows: [], + ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart new file mode 100644 index 0000000000..df8e0d46fb --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'relation_type_option_cubit.freezed.dart'; + +class RelationDatabaseListCubit extends Cubit { + RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) { + _loadDatabaseMetas(); + } + + void _loadDatabaseMetas() async { + final getDatabaseResult = await DatabaseEventGetDatabases().send(); + final metaPBs = getDatabaseResult.fold>( + (s) => s.items, + (f) => [], + ); + final futures = metaPBs.map((meta) { + return ViewBackendService.getView(meta.inlineViewId).then( + (result) => result.fold( + (s) => DatabaseMeta( + databaseId: meta.databaseId, + inlineViewId: meta.inlineViewId, + databaseName: s.name, + ), + (f) => null, + ), + ); + }); + final databaseMetas = await Future.wait(futures); + emit( + RelationDatabaseListState( + databaseMetas: databaseMetas.nonNulls.toList(), + ), + ); + } +} + +@freezed +class DatabaseMeta with _$DatabaseMeta { + factory DatabaseMeta({ + /// id of the database + required String databaseId, + + /// id of the inline view + required String inlineViewId, + + /// name of the database, currently identical to the name of the inline view + required String databaseName, + }) = _DatabaseMeta; +} + +@freezed +class RelationDatabaseListState with _$RelationDatabaseListState { + factory RelationDatabaseListState({ + required List databaseMetas, + }) = _RelationDatabaseListState; + + factory RelationDatabaseListState.initial() => + RelationDatabaseListState(databaseMetas: []); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart index 8d237f9114..8144904cad 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart @@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; class DatabaseBackendService { - static Future, FlowyError>> + static Future, FlowyError>> getAllDatabases() { return DatabaseEventGetDatabases().send().then((result) { return result.fold( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart index 3cc9bd1c72..4451a52b6f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart @@ -20,6 +20,7 @@ const List _supportedFieldTypes = [ FieldType.URL, FieldType.LastEditedTime, FieldType.CreatedTime, + FieldType.Relation, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart index e848200e4b..c408cb69de 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart @@ -1,15 +1,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:protobuf/protobuf.dart'; import 'builder.dart'; @@ -27,55 +27,76 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory { }) { final typeOption = _parseTypeOptionData(field.typeOptionData); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.only(left: 14, right: 8), - height: GridSize.popoverItemHeight, - alignment: Alignment.centerLeft, - child: FlowyText.regular( - LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(), - color: Theme.of(context).hintColor, - fontSize: 11, - ), - ), - AppFlowyPopover( - mutex: popoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(6, 0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - typeOption.databaseId.isEmpty - ? LocaleKeys.grid_relation_relatedDatabasePlaceholder.tr() - : typeOption.databaseId, - color: typeOption.databaseId.isEmpty - ? Theme.of(context).hintColor - : null, - overflow: TextOverflow.ellipsis, + return BlocProvider( + create: (_) => RelationDatabaseListCubit(), + child: Builder( + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.only(left: 14, right: 8), + height: GridSize.popoverItemHeight, + alignment: Alignment.centerLeft, + child: FlowyText.regular( + LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(), + color: Theme.of(context).hintColor, + fontSize: 11, + ), ), - rightIcon: const FlowySvg(FlowySvgs.more_s), - ), - ), - popupBuilder: (context) { - return _DatabaseList( - onSelectDatabase: (newDatabaseId) { - final newTypeOption = _updateTypeOption( - typeOption: typeOption, - databaseId: newDatabaseId, - ); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(context).close(); - }, - currentDatabaseId: - typeOption.databaseId.isEmpty ? null : typeOption.databaseId, - ); - }, - ), - ], + AppFlowyPopover( + mutex: popoverMutex, + triggerActions: + PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(6, 0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: BlocBuilder( + builder: (context, state) { + final databaseMeta = + state.databaseMetas.firstWhereOrNull( + (meta) => meta.databaseId == typeOption.databaseId, + ); + return FlowyText( + databaseMeta == null + ? LocaleKeys + .grid_relation_relatedDatabasePlaceholder + .tr() + : databaseMeta.databaseName, + color: databaseMeta == null + ? Theme.of(context).hintColor + : null, + overflow: TextOverflow.ellipsis, + ); + }, + ), + rightIcon: const FlowySvg(FlowySvgs.more_s), + ), + ), + popupBuilder: (popoverContext) { + return BlocProvider.value( + value: context.read(), + child: _DatabaseList( + onSelectDatabase: (newDatabaseId) { + final newTypeOption = _updateTypeOption( + typeOption: typeOption, + databaseId: newDatabaseId, + ); + onTypeOptionUpdated(newTypeOption.writeToBuffer()); + PopoverContainer.of(context).close(); + }, + currentDatabaseId: typeOption.databaseId, + ), + ); + }, + ), + ], + ); + }, + ), ); } @@ -94,65 +115,45 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory { } } -class _DatabaseList extends StatefulWidget { +class _DatabaseList extends StatelessWidget { const _DatabaseList({ required this.onSelectDatabase, required this.currentDatabaseId, }); - final String? currentDatabaseId; + final String currentDatabaseId; final void Function(String databaseId) onSelectDatabase; - @override - State<_DatabaseList> createState() => _DatabaseListState(); -} - -class _DatabaseListState extends State<_DatabaseList> { - late Future> future; - - @override - void initState() { - super.initState(); - future = DatabaseEventGetDatabases().send(); - } - @override Widget build(BuildContext context) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - final data = snapshot.data; - if (!snapshot.hasData || - snapshot.connectionState != ConnectionState.done || - data!.isFailure()) { - return const SizedBox.shrink(); - } - - final databaseIds = data - .fold>((l) => l.items, (r) => []) - .map((databaseDescription) { - final databaseId = databaseDescription.databaseId; - return FlowyButton( - onTap: () => widget.onSelectDatabase(databaseId), - text: FlowyText.medium( - databaseId, - overflow: TextOverflow.ellipsis, + return BlocBuilder( + builder: (context, state) { + final children = state.databaseMetas.map((meta) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + onTap: () => onSelectDatabase(meta.databaseId), + text: FlowyText.medium( + meta.databaseName, + overflow: TextOverflow.ellipsis, + ), + rightIcon: meta.databaseId == currentDatabaseId + ? FlowySvg( + FlowySvgs.check_s, + color: Theme.of(context).colorScheme.primary, + ) + : null, ), - rightIcon: databaseId == widget.currentDatabaseId - ? FlowySvg( - FlowySvgs.check_s, - color: Theme.of(context).colorScheme.primary, - ) - : null, ); }).toList(); return ListView.separated( shrinkWrap: true, + padding: EdgeInsets.zero, separatorBuilder: (_, __) => VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: databaseIds.length, - itemBuilder: (context, index) => databaseIds[index], + itemCount: children.length, + itemBuilder: (context, index) => children[index], ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart index b4619a7f47..023048355e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart @@ -1,8 +1,9 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -52,15 +53,19 @@ class _RelationCellState extends State { return const SizedBox.shrink(); } - final children = state.rows - .map( - (row) => FlowyText.medium( - row.name, + final children = state.rows.map( + (row) { + final isEmpty = row.name.isEmpty; + return Text( + isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, + style: widget.style.textStyle.copyWith( + color: isEmpty ? Theme.of(context).hintColor : null, decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, ), - ) - .toList(); + overflow: TextOverflow.ellipsis, + ); + }, + ).toList(); return Container( alignment: AlignmentDirectional.topStart, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart index f18717b58d..a2e6c9fa8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart @@ -1,10 +1,12 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/relation.dart'; @@ -29,10 +31,6 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { value: bloc, child: RelationCellEditor( selectedRowIds: state.rows.map((row) => row.rowId).toList(), - databaseId: state.relatedDatabaseId, - onSelectRow: (rowId) { - bloc.add(RelationCellEvent.selectRow(rowId)); - }, ), ); }, @@ -42,15 +40,17 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { child: Wrap( runSpacing: 4.0, spacing: 4.0, - children: state.rows - .map( - (row) => FlowyText.medium( - row.name, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ), - ) - .toList(), + children: state.rows.map( + (row) { + final isEmpty = row.name.isEmpty; + return FlowyText.medium( + isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, + color: isEmpty ? Theme.of(context).hintColor : null, + decoration: TextDecoration.underline, + overflow: TextOverflow.ellipsis, + ); + }, + ).toList(), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart index e545718080..63b70c0f78 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart @@ -1,9 +1,12 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/relation.dart'; @@ -26,36 +29,43 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { popupBuilder: (context) { return BlocProvider.value( value: bloc, - child: BlocBuilder( - builder: (context, state) => RelationCellEditor( - selectedRowIds: state.rows.map((row) => row.rowId).toList(), - databaseId: state.relatedDatabaseId, - onSelectRow: (rowId) { - context - .read() - .add(RelationCellEvent.selectRow(rowId)); - }, - ), + child: RelationCellEditor( + selectedRowIds: state.rows.map((row) => row.rowId).toList(), ), ); }, child: Container( alignment: AlignmentDirectional.centerStart, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: Wrap( - runSpacing: 4.0, - spacing: 4.0, - children: state.rows - .map( - (row) => FlowyText.medium( - row.name, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ), - ) - .toList(), - ), + child: state.rows.isEmpty + ? _buildPlaceholder(context) + : _buildRows(context, state.rows), ), ); } + + Widget _buildPlaceholder(BuildContext context) { + return FlowyText( + LocaleKeys.grid_row_textPlaceholder.tr(), + color: Theme.of(context).hintColor, + ); + } + + Widget _buildRows(BuildContext context, List rows) { + return Wrap( + runSpacing: 4.0, + spacing: 4.0, + children: rows.map( + (row) { + final isEmpty = row.name.isEmpty; + return FlowyText.medium( + isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, + color: isEmpty ? Theme.of(context).hintColor : null, + decoration: TextDecoration.underline, + overflow: TextOverflow.ellipsis, + ); + }, + ).toList(), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index 18182c6fb3..bece54ccc8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -13,38 +14,24 @@ import '../../application/cell/bloc/relation_row_search_bloc.dart'; class RelationCellEditor extends StatelessWidget { const RelationCellEditor({ super.key, - required this.databaseId, required this.selectedRowIds, - required this.onSelectRow, }); - final String databaseId; final List selectedRowIds; - final void Function(String rowId) onSelectRow; @override Widget build(BuildContext context) { - if (databaseId.isEmpty) { - // no i18n here because UX needs thorough checking. - return const Center( - child: FlowyText( - ''' -No database has been selected, -please select one first in the field editor. - ''', - maxLines: null, - textAlign: TextAlign.center, - ), - ); - } + return BlocBuilder( + builder: (context, cellState) { + if (cellState.relatedDatabaseMeta == null) { + return const _RelationCellEditorDatabaseList(); + } - return BlocProvider( - create: (context) => RelationRowSearchBloc( - databaseId: databaseId, - ), - child: BlocBuilder( - builder: (context, cellState) { - return BlocBuilder( + return BlocProvider( + create: (context) => RelationRowSearchBloc( + databaseId: cellState.relatedDatabaseMeta!.databaseId, + ), + child: BlocBuilder( builder: (context, state) { final children = state.filteredRows .map( @@ -68,7 +55,9 @@ please select one first in the field editor. color: Theme.of(context).primaryColor, ) : null, - onTap: () => onSelectRow(row.rowId), + onTap: () => context + .read() + .add(RelationCellEvent.selectRow(row.rowId)), ), ), ) @@ -78,7 +67,6 @@ please select one first in the field editor. mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const VSpace(6.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0) + GridSize.typeOptionContentInsets, @@ -90,15 +78,13 @@ please select one first in the field editor. fontSize: 11, color: Theme.of(context).hintColor, ), - const HSpace(2.0), - FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric( + Padding( + padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), - text: FlowyText.regular( - cellState.relatedDatabaseId, + child: FlowyText.regular( + cellState.relatedDatabaseMeta!.databaseName, fontSize: 11, overflow: TextOverflow.ellipsis, ), @@ -106,10 +92,16 @@ please select one first in the field editor. ], ), ), - VSpace(GridSize.typeOptionSeparatorHeight), Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: FlowyTextField( + hintText: LocaleKeys + .grid_relation_rowSearchTextFieldPlaceholder + .tr(), + hintStyle: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: Theme.of(context).hintColor), onChanged: (text) => context .read() .add(RelationRowSearchEvent.updateFilter(text)), @@ -140,6 +132,62 @@ please select one first in the field editor. ], ); }, + ), + ); + }, + ); + } +} + +class _RelationCellEditorDatabaseList extends StatelessWidget { + const _RelationCellEditorDatabaseList(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => RelationDatabaseListCubit(), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(6, 6, 6, 0), + child: FlowyText( + LocaleKeys.grid_relation_noDatabaseSelected.tr(), + maxLines: null, + fontSize: 10, + color: Theme.of(context).hintColor, + ), + ), + Flexible( + child: ListView.separated( + padding: const EdgeInsets.all(6), + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: state.databaseMetas.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final databaseMeta = state.databaseMetas[index]; + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + onTap: () => context.read().add( + RelationCellEvent.selectDatabaseId( + databaseMeta.databaseId, + ), + ), + text: FlowyText.medium( + databaseMeta.databaseName, + overflow: TextOverflow.ellipsis, + ), + ), + ); + }, + ), + ), + ], ); }, ), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a1fa0cc350..cdf07499e2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -736,6 +736,8 @@ "relatedDatabasePlaceLabel": "Related Database", "relatedDatabasePlaceholder": "None", "inRelatedDatabase": "In", + "rowSearchTextFieldPlaceholder": "Search", + "noDatabaseSelected": "No database selected, please select one first from the list below:", "emptySearchResult": "No records found" }, "menuName": "Grid", diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index 6849e0a8a3..1c2edd339d 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -812,6 +812,30 @@ async fn update_relation_cell_test() { .await; assert_eq!(cell.row_ids.len(), 3); + + // update the relation cell + let changeset = RelationCellChangesetPB { + view_id: grid_view.id.clone(), + cell_id: CellIdPB { + view_id: grid_view.id.clone(), + field_id: relation_field.id.clone(), + row_id: database.rows[0].id.clone(), + }, + removed_row_ids: vec![ + "row1rowid".to_string(), + "row3rowid".to_string(), + "row4rowid".to_string(), + ], + ..Default::default() + }; + test.update_relation_cell(changeset).await; + + // get the cell + let cell = test + .get_relation_cell(&grid_view.id, &relation_field.id, &database.rows[0].id) + .await; + + assert_eq!(cell.row_ids.len(), 1); } #[tokio::test] diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 2afcd41e05..dc82ba7cfa 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -1,6 +1,5 @@ use collab::core::collab_state::SyncState; use collab_database::rows::RowId; -use collab_database::user::DatabaseMeta; use collab_database::views::DatabaseLayout; use flowy_derive::ProtoBuf; @@ -203,23 +202,18 @@ impl TryInto for MoveGroupRowPayloadPB { } #[derive(Debug, Default, ProtoBuf)] -pub struct DatabaseDescriptionPB { +pub struct DatabaseMetaPB { #[pb(index = 1)] pub database_id: String, -} -impl From for DatabaseDescriptionPB { - fn from(data: DatabaseMeta) -> Self { - Self { - database_id: data.database_id, - } - } + #[pb(index = 2)] + pub inline_view_id: String, } #[derive(Debug, Default, ProtoBuf)] pub struct RepeatedDatabaseDescriptionPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } #[derive(Debug, Clone, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 6e6180b251..c3a714a0cf 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -3,6 +3,7 @@ use std::sync::{Arc, Weak}; use collab_database::rows::RowId; use lib_infra::box_any::BoxAny; use tokio::sync::oneshot; +use tracing::error; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{af_spawn, data_result_ok, AFPluginData, AFPluginState, DataResult}; @@ -741,7 +742,22 @@ pub(crate) async fn get_databases_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let data = manager.get_all_databases_description().await; + let metas = manager.get_all_databases_meta().await; + + let mut items = Vec::with_capacity(metas.len()); + for meta in metas { + match manager.get_database_inline_view_id(&meta.database_id).await { + Ok(view_id) => items.push(DatabaseMetaPB { + database_id: meta.database_id, + inline_view_id: view_id, + }), + Err(err) => { + error!(?err); + }, + } + } + + let data = RepeatedDatabaseDescriptionPB { items }; data_result_ok(data) } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 5a21e3b18a..bf5f1505f2 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -4,10 +4,10 @@ use std::sync::{Arc, Weak}; use collab::core::collab::{CollabDocState, MutexCollab}; use collab_database::blocks::BlockEvent; -use collab_database::database::{DatabaseData, MutexDatabase}; +use collab_database::database::{get_inline_view_id, DatabaseData, MutexDatabase}; use collab_database::error::DatabaseError; use collab_database::user::{ - CollabDocStateByOid, CollabFuture, DatabaseCollabService, WorkspaceDatabase, + CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase, }; use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; use collab_entity::CollabType; @@ -24,10 +24,7 @@ use flowy_error::{internal_error, FlowyError, FlowyResult}; use lib_dispatch::prelude::af_spawn; use lib_infra::priority_task::TaskDispatcher; -use crate::entities::{ - DatabaseDescriptionPB, DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB, - RepeatedDatabaseDescriptionPB, -}; +use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB}; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::database::DatabaseEditor; use crate::services::database_view::DatabaseLayoutDepsResolver; @@ -164,16 +161,27 @@ impl DatabaseManager { Ok(()) } - pub async fn get_all_databases_description(&self) -> RepeatedDatabaseDescriptionPB { + pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult { + let wdb = self.get_workspace_database().await?; + let database_collab = wdb.get_database_collab(database_id).await.ok_or_else(|| { + FlowyError::record_not_found().with_context(format!("The database:{} not found", database_id)) + })?; + + let inline_view_id = get_inline_view_id(&database_collab.lock()).ok_or_else(|| { + FlowyError::record_not_found().with_context(format!( + "Can't find the inline view for database:{}", + database_id + )) + })?; + Ok(inline_view_id) + } + + pub async fn get_all_databases_meta(&self) -> Vec { let mut items = vec![]; if let Ok(wdb) = self.get_workspace_database().await { - items = wdb - .get_all_database_meta() - .into_iter() - .map(DatabaseDescriptionPB::from) - .collect(); + items = wdb.get_all_database_meta() } - RepeatedDatabaseDescriptionPB { items } + items } pub async fn track_database( diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 83530dc547..58064018e1 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -103,7 +103,7 @@ pub(crate) async fn create_orphan_view_handler( } #[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn read_view_handler( +pub(crate) async fn get_view_handler( data: AFPluginData, folder: AFPluginState>, ) -> DataResult { diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index b97a386e33..e81afbb656 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -17,7 +17,7 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) .event(FolderEvent::CreateView, create_view_handler) .event(FolderEvent::CreateOrphanView, create_orphan_view_handler) - .event(FolderEvent::GetView, read_view_handler) + .event(FolderEvent::GetView, get_view_handler) .event(FolderEvent::UpdateView, update_view_handler) .event(FolderEvent::DeleteView, delete_view_handler) .event(FolderEvent::DuplicateView, duplicate_view_handler) diff --git a/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs index 4761b55b73..464872803f 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs @@ -164,8 +164,8 @@ where let new_object_id = &new_user_session.user_workspace.workspace_database_object_id; let array = DatabaseMetaList::from_collab(&database_with_views_collab); - for database_metas in array.get_all_database_meta() { - array.update_database(&database_metas.database_id, |update| { + for database_meta in array.get_all_database_meta() { + array.update_database(&database_meta.database_id, |update| { let new_linked_views = update .linked_views .iter() diff --git a/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs index 5fb01aae5a..88c1b340cc 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs @@ -75,7 +75,7 @@ pub async fn sync_supabase_user_data_to_cloud( fn sync_view( uid: i64, folder: Arc, - database_records: Vec>, + database_metas: Vec>, workspace_id: String, device_id: String, view: Arc, @@ -84,7 +84,7 @@ fn sync_view( ) -> Pin> + Send + Sync>> { Box::pin(async move { let collab_type = collab_type_from_view_layout(&view.layout); - let object_id = object_id_from_view(&view, &database_records)?; + let object_id = object_id_from_view(&view, &database_metas)?; tracing::debug!( "sync view: {:?}:{} with object_id: {}", view.layout, @@ -180,7 +180,7 @@ fn sync_view( if let Err(err) = Box::pin(sync_view( uid, folder.clone(), - database_records.clone(), + database_metas.clone(), workspace_id.clone(), device_id.to_string(), child_view, diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index e48df18254..6e98a2adab 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -281,10 +281,10 @@ where })?; let array = DatabaseMetaList::from_collab(&database_view_tracker_collab); - for database_metas in array.get_all_database_meta() { + for database_meta in array.get_all_database_meta() { database_view_ids_by_database_id.insert( - old_to_new_id_map.renew_id(&database_metas.database_id), - database_metas + old_to_new_id_map.renew_id(&database_meta.database_id), + database_meta .linked_views .into_iter() .map(|view_id| old_to_new_id_map.renew_id(&view_id))