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>
This commit is contained in:
Nathan.fooo 2024-03-15 22:58:55 +08:00 committed by GitHub
parent 8d01d54e7f
commit bb414c3fd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 459 additions and 231 deletions

View File

@ -1,10 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; 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/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/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -35,12 +39,14 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
(event, emit) async { (event, emit) async {
await event.when( await event.when(
didUpdateCell: (RelationCellDataPB? cellData) async { didUpdateCell: (RelationCellDataPB? cellData) async {
if (cellData == null || cellData.rowIds.isEmpty) { if (cellData == null ||
cellData.rowIds.isEmpty ||
state.relatedDatabaseMeta == null) {
emit(state.copyWith(rows: const [])); emit(state.copyWith(rows: const []));
return; return;
} }
final payload = RepeatedRowIdPB( final payload = RepeatedRowIdPB(
databaseId: state.relatedDatabaseId, databaseId: state.relatedDatabaseMeta!.databaseId,
rowIds: cellData.rowIds, rowIds: cellData.rowIds,
); );
final result = final result =
@ -54,8 +60,16 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
); );
emit(state.copyWith(rows: rows)); emit(state.copyWith(rows: rows));
}, },
didUpdateRelationDatabaseId: (databaseId) { didUpdateRelationTypeOption: (typeOption) async {
emit(state.copyWith(relatedDatabaseId: databaseId)); 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 { selectRow: (rowId) async {
await _handleSelectRow(rowId); await _handleSelectRow(rowId);
@ -73,29 +87,30 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
} }
}, },
onCellFieldChanged: (field) { onCellFieldChanged: (field) {
if (!isClosed) { // hack: SingleFieldListener receives notification before
// hack: SingleFieldListener receives notification before // FieldController's copy is updated.
// FieldController's copy is updated. Future.delayed(const Duration(milliseconds: 50), () {
Future.delayed(const Duration(milliseconds: 50), () { if (!isClosed) {
final RelationTypeOptionPB typeOption = final RelationTypeOptionPB typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser()); cellController.getTypeOption(RelationTypeOptionDataParser());
add( add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
RelationCellEvent.didUpdateRelationDatabaseId( }
typeOption.databaseId, });
),
);
});
}
}, },
); );
} }
void _init() { void _init() {
final RelationTypeOptionPB typeOption = final typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser()); cellController.getTypeOption(RelationTypeOptionDataParser());
add(RelationCellEvent.didUpdateRelationDatabaseId(typeOption.databaseId)); add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
}
void _loadCellData() {
final cellData = cellController.getCellData(); final cellData = cellController.getCellData();
add(RelationCellEvent.didUpdateCell(cellData)); if (!isClosed) {
add(RelationCellEvent.didUpdateCell(cellData));
}
} }
Future<void> _handleSelectRow(String rowId) async { Future<void> _handleSelectRow(String rowId) async {
@ -115,25 +130,66 @@ class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
final result = await DatabaseEventUpdateRelationCell(payload).send(); final result = await DatabaseEventUpdateRelationCell(payload).send();
result.fold((l) => null, (err) => Log.error(err)); result.fold((l) => null, (err) => Log.error(err));
} }
Future<DatabaseMeta?> _loadDatabaseMeta(String databaseId) async {
final getDatabaseResult = await DatabaseEventGetDatabases().send();
final databaseMeta = getDatabaseResult.fold<DatabaseMetaPB?>(
(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<void> _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 @freezed
class RelationCellEvent with _$RelationCellEvent { class RelationCellEvent with _$RelationCellEvent {
const factory RelationCellEvent.didUpdateRelationDatabaseId( const factory RelationCellEvent.didUpdateRelationTypeOption(
String databaseId, RelationTypeOptionPB typeOption,
) = _DidUpdateRelationDatabaseId; ) = _DidUpdateRelationTypeOption;
const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) = const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) =
_DidUpdateCell; _DidUpdateCell;
const factory RelationCellEvent.selectDatabaseId(
String databaseId,
) = _SelectDatabaseId;
const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId; const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId;
} }
@freezed @freezed
class RelationCellState with _$RelationCellState { class RelationCellState with _$RelationCellState {
const factory RelationCellState({ const factory RelationCellState({
required String relatedDatabaseId, required DatabaseMeta? relatedDatabaseMeta,
required List<RelatedRowDataPB> rows, required List<RelatedRowDataPB> rows,
}) = _RelationCellState; }) = _RelationCellState;
factory RelationCellState.initial() => factory RelationCellState.initial() => const RelationCellState(
const RelationCellState(relatedDatabaseId: "", rows: []); relatedDatabaseMeta: null,
rows: [],
);
} }

View File

@ -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<RelationDatabaseListState> {
RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) {
_loadDatabaseMetas();
}
void _loadDatabaseMetas() async {
final getDatabaseResult = await DatabaseEventGetDatabases().send();
final metaPBs = getDatabaseResult.fold<List<DatabaseMetaPB>>(
(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<DatabaseMeta> databaseMetas,
}) = _RelationDatabaseListState;
factory RelationDatabaseListState.initial() =>
RelationDatabaseListState(databaseMetas: []);
}

View File

@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
class DatabaseBackendService { class DatabaseBackendService {
static Future<FlowyResult<List<DatabaseDescriptionPB>, FlowyError>> static Future<FlowyResult<List<DatabaseMetaPB>, FlowyError>>
getAllDatabases() { getAllDatabases() {
return DatabaseEventGetDatabases().send().then((result) { return DatabaseEventGetDatabases().send().then((result) {
return result.fold( return result.fold(

View File

@ -20,6 +20,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.URL, FieldType.URL,
FieldType.LastEditedTime, FieldType.LastEditedTime,
FieldType.CreatedTime, FieldType.CreatedTime,
FieldType.Relation,
]; ];
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {

View File

@ -1,15 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.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-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
import 'builder.dart'; import 'builder.dart';
@ -27,55 +27,76 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
}) { }) {
final typeOption = _parseTypeOptionData(field.typeOptionData); final typeOption = _parseTypeOptionData(field.typeOptionData);
return Column( return BlocProvider(
mainAxisSize: MainAxisSize.min, create: (_) => RelationDatabaseListCubit(),
children: [ child: Builder(
Container( builder: (context) {
padding: const EdgeInsets.only(left: 14, right: 8), return Column(
height: GridSize.popoverItemHeight, mainAxisSize: MainAxisSize.min,
alignment: Alignment.centerLeft, children: [
child: FlowyText.regular( Container(
LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(), padding: const EdgeInsets.only(left: 14, right: 8),
color: Theme.of(context).hintColor, height: GridSize.popoverItemHeight,
fontSize: 11, alignment: Alignment.centerLeft,
), child: FlowyText.regular(
), LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(),
AppFlowyPopover( color: Theme.of(context).hintColor,
mutex: popoverMutex, fontSize: 11,
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,
), ),
rightIcon: const FlowySvg(FlowySvgs.more_s), AppFlowyPopover(
), mutex: popoverMutex,
), triggerActions:
popupBuilder: (context) { PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
return _DatabaseList( offset: const Offset(6, 0),
onSelectDatabase: (newDatabaseId) { child: Container(
final newTypeOption = _updateTypeOption( padding: const EdgeInsets.symmetric(horizontal: 8),
typeOption: typeOption, height: GridSize.popoverItemHeight,
databaseId: newDatabaseId, child: FlowyButton(
); text: BlocBuilder<RelationDatabaseListCubit,
onTypeOptionUpdated(newTypeOption.writeToBuffer()); RelationDatabaseListState>(
PopoverContainer.of(context).close(); builder: (context, state) {
}, final databaseMeta =
currentDatabaseId: state.databaseMetas.firstWhereOrNull(
typeOption.databaseId.isEmpty ? null : typeOption.databaseId, (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<RelationDatabaseListCubit>(),
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({ const _DatabaseList({
required this.onSelectDatabase, required this.onSelectDatabase,
required this.currentDatabaseId, required this.currentDatabaseId,
}); });
final String? currentDatabaseId; final String currentDatabaseId;
final void Function(String databaseId) onSelectDatabase; final void Function(String databaseId) onSelectDatabase;
@override
State<_DatabaseList> createState() => _DatabaseListState();
}
class _DatabaseListState extends State<_DatabaseList> {
late Future<FlowyResult<RepeatedDatabaseDescriptionPB, FlowyError>> future;
@override
void initState() {
super.initState();
future = DatabaseEventGetDatabases().send();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return BlocBuilder<RelationDatabaseListCubit, RelationDatabaseListState>(
future: future, builder: (context, state) {
builder: (context, snapshot) { final children = state.databaseMetas.map((meta) {
final data = snapshot.data; return SizedBox(
if (!snapshot.hasData || height: GridSize.popoverItemHeight,
snapshot.connectionState != ConnectionState.done || child: FlowyButton(
data!.isFailure()) { onTap: () => onSelectDatabase(meta.databaseId),
return const SizedBox.shrink(); text: FlowyText.medium(
} meta.databaseName,
overflow: TextOverflow.ellipsis,
final databaseIds = data ),
.fold<List<DatabaseDescriptionPB>>((l) => l.items, (r) => []) rightIcon: meta.databaseId == currentDatabaseId
.map((databaseDescription) { ? FlowySvg(
final databaseId = databaseDescription.databaseId; FlowySvgs.check_s,
return FlowyButton( color: Theme.of(context).colorScheme.primary,
onTap: () => widget.onSelectDatabase(databaseId), )
text: FlowyText.medium( : null,
databaseId,
overflow: TextOverflow.ellipsis,
), ),
rightIcon: databaseId == widget.currentDatabaseId
? FlowySvg(
FlowySvgs.check_s,
color: Theme.of(context).colorScheme.primary,
)
: null,
); );
}).toList(); }).toList();
return ListView.separated( return ListView.separated(
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.zero,
separatorBuilder: (_, __) => separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: databaseIds.length, itemCount: children.length,
itemBuilder: (context, index) => databaseIds[index], itemBuilder: (context, index) => children[index],
); );
}, },
); );

View File

@ -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.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.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/database_controller.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -52,15 +53,19 @@ class _RelationCellState extends State<RelationCardCell> {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final children = state.rows final children = state.rows.map(
.map( (row) {
(row) => FlowyText.medium( final isEmpty = row.name.isEmpty;
row.name, 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, decoration: TextDecoration.underline,
overflow: TextOverflow.ellipsis,
), ),
) overflow: TextOverflow.ellipsis,
.toList(); );
},
).toList();
return Container( return Container(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,

View File

@ -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/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.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/widgets/cell_editor/relation_cell_editor.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.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: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 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/relation.dart'; import '../editable_cell_skeleton/relation.dart';
@ -29,10 +31,6 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin {
value: bloc, value: bloc,
child: RelationCellEditor( child: RelationCellEditor(
selectedRowIds: state.rows.map((row) => row.rowId).toList(), 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( child: Wrap(
runSpacing: 4.0, runSpacing: 4.0,
spacing: 4.0, spacing: 4.0,
children: state.rows children: state.rows.map(
.map( (row) {
(row) => FlowyText.medium( final isEmpty = row.name.isEmpty;
row.name, return FlowyText.medium(
decoration: TextDecoration.underline, isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,
overflow: TextOverflow.ellipsis, color: isEmpty ? Theme.of(context).hintColor : null,
), decoration: TextDecoration.underline,
) overflow: TextOverflow.ellipsis,
.toList(), );
},
).toList(),
), ),
), ),
); );

View File

@ -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/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.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/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:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/relation.dart'; import '../editable_cell_skeleton/relation.dart';
@ -26,36 +29,43 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin {
popupBuilder: (context) { popupBuilder: (context) {
return BlocProvider.value( return BlocProvider.value(
value: bloc, value: bloc,
child: BlocBuilder<RelationCellBloc, RelationCellState>( child: RelationCellEditor(
builder: (context, state) => RelationCellEditor( selectedRowIds: state.rows.map((row) => row.rowId).toList(),
selectedRowIds: state.rows.map((row) => row.rowId).toList(),
databaseId: state.relatedDatabaseId,
onSelectRow: (rowId) {
context
.read<RelationCellBloc>()
.add(RelationCellEvent.selectRow(rowId));
},
),
), ),
); );
}, },
child: Container( child: Container(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Wrap( child: state.rows.isEmpty
runSpacing: 4.0, ? _buildPlaceholder(context)
spacing: 4.0, : _buildRows(context, state.rows),
children: state.rows
.map(
(row) => FlowyText.medium(
row.name,
decoration: TextDecoration.underline,
overflow: TextOverflow.ellipsis,
),
)
.toList(),
),
), ),
); );
} }
Widget _buildPlaceholder(BuildContext context) {
return FlowyText(
LocaleKeys.grid_row_textPlaceholder.tr(),
color: Theme.of(context).hintColor,
);
}
Widget _buildRows(BuildContext context, List<RelatedRowDataPB> 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(),
);
}
} }

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:easy_localization/easy_localization.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 { class RelationCellEditor extends StatelessWidget {
const RelationCellEditor({ const RelationCellEditor({
super.key, super.key,
required this.databaseId,
required this.selectedRowIds, required this.selectedRowIds,
required this.onSelectRow,
}); });
final String databaseId;
final List<String> selectedRowIds; final List<String> selectedRowIds;
final void Function(String rowId) onSelectRow;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (databaseId.isEmpty) { return BlocBuilder<RelationCellBloc, RelationCellState>(
// no i18n here because UX needs thorough checking. builder: (context, cellState) {
return const Center( if (cellState.relatedDatabaseMeta == null) {
child: FlowyText( return const _RelationCellEditorDatabaseList();
''' }
No database has been selected,
please select one first in the field editor.
''',
maxLines: null,
textAlign: TextAlign.center,
),
);
}
return BlocProvider<RelationRowSearchBloc>( return BlocProvider<RelationRowSearchBloc>(
create: (context) => RelationRowSearchBloc( create: (context) => RelationRowSearchBloc(
databaseId: databaseId, databaseId: cellState.relatedDatabaseMeta!.databaseId,
), ),
child: BlocBuilder<RelationCellBloc, RelationCellState>( child: BlocBuilder<RelationRowSearchBloc, RelationRowSearchState>(
builder: (context, cellState) {
return BlocBuilder<RelationRowSearchBloc, RelationRowSearchState>(
builder: (context, state) { builder: (context, state) {
final children = state.filteredRows final children = state.filteredRows
.map( .map(
@ -68,7 +55,9 @@ please select one first in the field editor.
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
) )
: null, : null,
onTap: () => onSelectRow(row.rowId), onTap: () => context
.read<RelationCellBloc>()
.add(RelationCellEvent.selectRow(row.rowId)),
), ),
), ),
) )
@ -78,7 +67,6 @@ please select one first in the field editor.
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const VSpace(6.0),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0) + padding: const EdgeInsets.symmetric(horizontal: 6.0) +
GridSize.typeOptionContentInsets, GridSize.typeOptionContentInsets,
@ -90,15 +78,13 @@ please select one first in the field editor.
fontSize: 11, fontSize: 11,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
const HSpace(2.0), Padding(
FlowyButton( padding: const EdgeInsets.symmetric(
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(
horizontal: 4, horizontal: 4,
vertical: 2, vertical: 2,
), ),
text: FlowyText.regular( child: FlowyText.regular(
cellState.relatedDatabaseId, cellState.relatedDatabaseMeta!.databaseName,
fontSize: 11, fontSize: 11,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -106,10 +92,16 @@ please select one first in the field editor.
], ],
), ),
), ),
VSpace(GridSize.typeOptionSeparatorHeight),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0), padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: FlowyTextField( child: FlowyTextField(
hintText: LocaleKeys
.grid_relation_rowSearchTextFieldPlaceholder
.tr(),
hintStyle: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Theme.of(context).hintColor),
onChanged: (text) => context onChanged: (text) => context
.read<RelationRowSearchBloc>() .read<RelationRowSearchBloc>()
.add(RelationRowSearchEvent.updateFilter(text)), .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<RelationDatabaseListCubit, RelationDatabaseListState>(
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<RelationCellBloc>().add(
RelationCellEvent.selectDatabaseId(
databaseMeta.databaseId,
),
),
text: FlowyText.medium(
databaseMeta.databaseName,
overflow: TextOverflow.ellipsis,
),
),
);
},
),
),
],
); );
}, },
), ),

View File

@ -736,6 +736,8 @@
"relatedDatabasePlaceLabel": "Related Database", "relatedDatabasePlaceLabel": "Related Database",
"relatedDatabasePlaceholder": "None", "relatedDatabasePlaceholder": "None",
"inRelatedDatabase": "In", "inRelatedDatabase": "In",
"rowSearchTextFieldPlaceholder": "Search",
"noDatabaseSelected": "No database selected, please select one first from the list below:",
"emptySearchResult": "No records found" "emptySearchResult": "No records found"
}, },
"menuName": "Grid", "menuName": "Grid",

View File

@ -812,6 +812,30 @@ async fn update_relation_cell_test() {
.await; .await;
assert_eq!(cell.row_ids.len(), 3); 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] #[tokio::test]

View File

@ -1,6 +1,5 @@
use collab::core::collab_state::SyncState; use collab::core::collab_state::SyncState;
use collab_database::rows::RowId; use collab_database::rows::RowId;
use collab_database::user::DatabaseMeta;
use collab_database::views::DatabaseLayout; use collab_database::views::DatabaseLayout;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
@ -203,23 +202,18 @@ impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
} }
#[derive(Debug, Default, ProtoBuf)] #[derive(Debug, Default, ProtoBuf)]
pub struct DatabaseDescriptionPB { pub struct DatabaseMetaPB {
#[pb(index = 1)] #[pb(index = 1)]
pub database_id: String, pub database_id: String,
}
impl From<DatabaseMeta> for DatabaseDescriptionPB { #[pb(index = 2)]
fn from(data: DatabaseMeta) -> Self { pub inline_view_id: String,
Self {
database_id: data.database_id,
}
}
} }
#[derive(Debug, Default, ProtoBuf)] #[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedDatabaseDescriptionPB { pub struct RepeatedDatabaseDescriptionPB {
#[pb(index = 1)] #[pb(index = 1)]
pub items: Vec<DatabaseDescriptionPB>, pub items: Vec<DatabaseMetaPB>,
} }
#[derive(Debug, Clone, Default, ProtoBuf)] #[derive(Debug, Clone, Default, ProtoBuf)]

View File

@ -3,6 +3,7 @@ use std::sync::{Arc, Weak};
use collab_database::rows::RowId; use collab_database::rows::RowId;
use lib_infra::box_any::BoxAny; use lib_infra::box_any::BoxAny;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tracing::error;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{af_spawn, data_result_ok, AFPluginData, AFPluginState, DataResult}; 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<Weak<DatabaseManager>>, manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<RepeatedDatabaseDescriptionPB, FlowyError> { ) -> DataResult<RepeatedDatabaseDescriptionPB, FlowyError> {
let manager = upgrade_manager(manager)?; 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) data_result_ok(data)
} }

View File

@ -4,10 +4,10 @@ use std::sync::{Arc, Weak};
use collab::core::collab::{CollabDocState, MutexCollab}; use collab::core::collab::{CollabDocState, MutexCollab};
use collab_database::blocks::BlockEvent; 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::error::DatabaseError;
use collab_database::user::{ use collab_database::user::{
CollabDocStateByOid, CollabFuture, DatabaseCollabService, WorkspaceDatabase, CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase,
}; };
use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout};
use collab_entity::CollabType; use collab_entity::CollabType;
@ -24,10 +24,7 @@ use flowy_error::{internal_error, FlowyError, FlowyResult};
use lib_dispatch::prelude::af_spawn; use lib_dispatch::prelude::af_spawn;
use lib_infra::priority_task::TaskDispatcher; use lib_infra::priority_task::TaskDispatcher;
use crate::entities::{ use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB};
DatabaseDescriptionPB, DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB,
RepeatedDatabaseDescriptionPB,
};
use crate::notification::{send_notification, DatabaseNotification}; use crate::notification::{send_notification, DatabaseNotification};
use crate::services::database::DatabaseEditor; use crate::services::database::DatabaseEditor;
use crate::services::database_view::DatabaseLayoutDepsResolver; use crate::services::database_view::DatabaseLayoutDepsResolver;
@ -164,16 +161,27 @@ impl DatabaseManager {
Ok(()) Ok(())
} }
pub async fn get_all_databases_description(&self) -> RepeatedDatabaseDescriptionPB { pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult<String> {
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<DatabaseMeta> {
let mut items = vec![]; let mut items = vec![];
if let Ok(wdb) = self.get_workspace_database().await { if let Ok(wdb) = self.get_workspace_database().await {
items = wdb items = wdb.get_all_database_meta()
.get_all_database_meta()
.into_iter()
.map(DatabaseDescriptionPB::from)
.collect();
} }
RepeatedDatabaseDescriptionPB { items } items
} }
pub async fn track_database( pub async fn track_database(

View File

@ -103,7 +103,7 @@ pub(crate) async fn create_orphan_view_handler(
} }
#[tracing::instrument(level = "debug", skip(data, folder), err)] #[tracing::instrument(level = "debug", skip(data, folder), err)]
pub(crate) async fn read_view_handler( pub(crate) async fn get_view_handler(
data: AFPluginData<ViewIdPB>, data: AFPluginData<ViewIdPB>,
folder: AFPluginState<Weak<FolderManager>>, folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<ViewPB, FlowyError> { ) -> DataResult<ViewPB, FlowyError> {

View File

@ -17,7 +17,7 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
.event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler)
.event(FolderEvent::CreateView, create_view_handler) .event(FolderEvent::CreateView, create_view_handler)
.event(FolderEvent::CreateOrphanView, create_orphan_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::UpdateView, update_view_handler)
.event(FolderEvent::DeleteView, delete_view_handler) .event(FolderEvent::DeleteView, delete_view_handler)
.event(FolderEvent::DuplicateView, duplicate_view_handler) .event(FolderEvent::DuplicateView, duplicate_view_handler)

View File

@ -164,8 +164,8 @@ where
let new_object_id = &new_user_session.user_workspace.workspace_database_object_id; let new_object_id = &new_user_session.user_workspace.workspace_database_object_id;
let array = DatabaseMetaList::from_collab(&database_with_views_collab); let array = DatabaseMetaList::from_collab(&database_with_views_collab);
for database_metas in array.get_all_database_meta() { for database_meta in array.get_all_database_meta() {
array.update_database(&database_metas.database_id, |update| { array.update_database(&database_meta.database_id, |update| {
let new_linked_views = update let new_linked_views = update
.linked_views .linked_views
.iter() .iter()

View File

@ -75,7 +75,7 @@ pub async fn sync_supabase_user_data_to_cloud(
fn sync_view( fn sync_view(
uid: i64, uid: i64,
folder: Arc<MutexFolder>, folder: Arc<MutexFolder>,
database_records: Vec<Arc<DatabaseMeta>>, database_metas: Vec<Arc<DatabaseMeta>>,
workspace_id: String, workspace_id: String,
device_id: String, device_id: String,
view: Arc<View>, view: Arc<View>,
@ -84,7 +84,7 @@ fn sync_view(
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + Sync>> { ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + Sync>> {
Box::pin(async move { Box::pin(async move {
let collab_type = collab_type_from_view_layout(&view.layout); 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!( tracing::debug!(
"sync view: {:?}:{} with object_id: {}", "sync view: {:?}:{} with object_id: {}",
view.layout, view.layout,
@ -180,7 +180,7 @@ fn sync_view(
if let Err(err) = Box::pin(sync_view( if let Err(err) = Box::pin(sync_view(
uid, uid,
folder.clone(), folder.clone(),
database_records.clone(), database_metas.clone(),
workspace_id.clone(), workspace_id.clone(),
device_id.to_string(), device_id.to_string(),
child_view, child_view,

View File

@ -281,10 +281,10 @@ where
})?; })?;
let array = DatabaseMetaList::from_collab(&database_view_tracker_collab); 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( database_view_ids_by_database_id.insert(
old_to_new_id_map.renew_id(&database_metas.database_id), old_to_new_id_map.renew_id(&database_meta.database_id),
database_metas database_meta
.linked_views .linked_views
.into_iter() .into_iter()
.map(|view_id| old_to_new_id_map.renew_id(&view_id)) .map(|view_id| old_to_new_id_map.renew_id(&view_id))