Skip to content

Commit

Permalink
feat: add basic relation field (AppFlowy-IO#4397)
Browse files Browse the repository at this point in the history
* feat: add basic relation field

* fix: clippy

* fix: tauri build 🤞

* chore: merge changes

* fix: merge main

* chore: initial code review pass

* fix: rust-lib test

* chore: code cleanup

* fix: unwrap or default
  • Loading branch information
richardshiue authored Feb 29, 2024
1 parent f826d05 commit f4ca3ef
Show file tree
Hide file tree
Showing 54 changed files with 1,804 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import 'dart:async';

import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.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:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'relation_cell_bloc.freezed.dart';

class RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {
RelationCellBloc({required this.cellController})
: super(RelationCellState.initial()) {
_dispatch();
_startListening();
_init();
}

final RelationCellController cellController;
void Function()? _onCellChangedFn;

@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
return super.close();
}

void _dispatch() {
on<RelationCellEvent>(
(event, emit) async {
await event.when(
didUpdateCell: (RelationCellDataPB? cellData) async {
if (cellData == null || cellData.rowIds.isEmpty) {
emit(state.copyWith(rows: const []));
return;
}
final payload = RepeatedRowIdPB(
databaseId: state.relatedDatabaseId,
rowIds: cellData.rowIds,
);
final result =
await DatabaseEventGetRelatedRowDatas(payload).send();
final rows = result.fold(
(data) => data.rows,
(err) {
Log.error(err);
return const <RelatedRowDataPB>[];
},
);
emit(state.copyWith(rows: rows));
},
didUpdateRelationDatabaseId: (databaseId) {
emit(state.copyWith(relatedDatabaseId: databaseId));
},
selectRow: (rowId) async {
await _handleSelectRow(rowId);
},
);
},
);
}

void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (data) {
if (!isClosed) {
add(RelationCellEvent.didUpdateCell(data));
}
},
onCellFieldChanged: (field) {
if (!isClosed) {
// hack: SingleFieldListener receives notification before
// FieldController's copy is updated.
Future.delayed(const Duration(milliseconds: 50), () {
final RelationTypeOptionPB typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser());
add(
RelationCellEvent.didUpdateRelationDatabaseId(
typeOption.databaseId,
),
);
});
}
},
);
}

void _init() {
final RelationTypeOptionPB typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser());
add(RelationCellEvent.didUpdateRelationDatabaseId(typeOption.databaseId));
final cellData = cellController.getCellData();
add(RelationCellEvent.didUpdateCell(cellData));
}

Future<void> _handleSelectRow(String rowId) async {
final payload = RelationCellChangesetPB(
viewId: cellController.viewId,
cellId: CellIdPB(
viewId: cellController.viewId,
fieldId: cellController.fieldId,
rowId: cellController.rowId,
),
);
if (state.rows.any((row) => row.rowId == rowId)) {
payload.removedRowIds.add(rowId);
} else {
payload.insertedRowIds.add(rowId);
}
final result = await DatabaseEventUpdateRelationCell(payload).send();
result.fold((l) => null, (err) => Log.error(err));
}
}

@freezed
class RelationCellEvent with _$RelationCellEvent {
const factory RelationCellEvent.didUpdateRelationDatabaseId(
String databaseId,
) = _DidUpdateRelationDatabaseId;
const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) =
_DidUpdateCell;
const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId;
}

@freezed
class RelationCellState with _$RelationCellState {
const factory RelationCellState({
required String relatedDatabaseId,
required List<RelatedRowDataPB> rows,
}) = _RelationCellState;

factory RelationCellState.initial() =>
const RelationCellState(relatedDatabaseId: "", rows: []);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'relation_row_search_bloc.freezed.dart';

class RelationRowSearchBloc
extends Bloc<RelationRowSearchEvent, RelationRowSearchState> {
RelationRowSearchBloc({
required this.databaseId,
}) : super(RelationRowSearchState.initial()) {
_dispatch();
_init();
}

final String databaseId;
final List<RelatedRowDataPB> allRows = [];

void _dispatch() {
on<RelationRowSearchEvent>(
(event, emit) {
event.when(
didUpdateRowList: (List<RelatedRowDataPB> rowList) {
allRows.clear();
allRows.addAll(rowList);
emit(state.copyWith(filteredRows: allRows));
},
updateFilter: (String filter) => _updateFilter(filter, emit),
);
},
);
}

Future<void> _init() async {
final payload = DatabaseIdPB(value: databaseId);
final result = await DatabaseEventGetRelatedDatabaseRows(payload).send();
result.fold(
(data) => add(RelationRowSearchEvent.didUpdateRowList(data.rows)),
(err) => Log.error(err),
);
}

void _updateFilter(String filter, Emitter<RelationRowSearchState> emit) {
final rows = [...allRows];
if (filter.isNotEmpty) {
rows.retainWhere(
(row) => row.name.toLowerCase().contains(filter.toLowerCase()),
);
}
emit(state.copyWith(filter: filter, filteredRows: rows));
}
}

@freezed
class RelationRowSearchEvent with _$RelationRowSearchEvent {
const factory RelationRowSearchEvent.didUpdateRowList(
List<RelatedRowDataPB> rowList,
) = _DidUpdateRowList;
const factory RelationRowSearchEvent.updateFilter(String filter) =
_UpdateFilter;
}

@freezed
class RelationRowSearchState with _$RelationRowSearchState {
const factory RelationRowSearchState({
required String filter,
required List<RelatedRowDataPB> filteredRows,
}) = _RelationRowSearchState;

factory RelationRowSearchState.initial() => const RelationRowSearchState(
filter: "",
filteredRows: [],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
typedef DateCellController = CellController<DateCellDataPB, String>;
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
typedef URLCellController = CellController<URLCellDataPB, String>;
typedef RelationCellController = CellController<RelationCellDataPB, String>;

CellController makeCellController(
DatabaseController databaseController,
Expand Down Expand Up @@ -118,6 +119,19 @@ CellController makeCellController(
),
cellDataPersistence: TextCellDataPersistence(),
);

case FieldType.Relation:
return RelationCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: RelationCellDataParser(),
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
}
throw UnimplementedError;
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,10 @@ class URLCellDataParser implements CellDataParser<URLCellDataPB> {
return URLCellDataPB.fromBuffer(data);
}
}

class RelationCellDataParser implements CellDataParser<RelationCellDataPB> {
@override
RelationCellDataPB? parserData(List<int> data) {
return data.isEmpty ? null : RelationCellDataPB.fromBuffer(data);
}
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,11 @@ class ChecklistTypeOptionDataParser
return ChecklistTypeOptionPB.fromBuffer(buffer);
}
}

class RelationTypeOptionDataParser
extends TypeOptionParser<RelationTypeOptionPB> {
@override
RelationTypeOptionPB fromBuffer(List<int> buffer) {
return RelationTypeOptionPB.fromBuffer(buffer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ class GridCreateFilterBloc
fieldId: fieldId,
condition: TextFilterConditionPB.Contains,
);
default:
throw UnimplementedError();
}

return FlowyResult.success(null);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,27 @@ import '../../layout/sizes.dart';

typedef SelectFieldCallback = void Function(FieldType);

const List<FieldType> _supportedFieldTypes = [
FieldType.RichText,
FieldType.Number,
FieldType.SingleSelect,
FieldType.MultiSelect,
FieldType.DateTime,
FieldType.Checkbox,
FieldType.Checklist,
FieldType.URL,
FieldType.LastEditedTime,
FieldType.CreatedTime,
];

class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
const FieldTypeList({required this.onSelectField, super.key});

final SelectFieldCallback onSelectField;

@override
Widget build(BuildContext context) {
final cells = FieldType.values.map((fieldType) {
final cells = _supportedFieldTypes.map((fieldType) {
return FieldTypeCell(
fieldType: fieldType,
onSelectField: (fieldType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'checklist.dart';
import 'date.dart';
import 'multi_select.dart';
import 'number.dart';
import 'relation.dart';
import 'rich_text.dart';
import 'single_select.dart';
import 'timestamp.dart';
Expand All @@ -29,6 +30,7 @@ abstract class TypeOptionEditorFactory {
FieldType.MultiSelect => const MultiSelectTypeOptionEditorFactory(),
FieldType.Checkbox => const CheckboxTypeOptionEditorFactory(),
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
FieldType.Relation => const RelationTypeOptionEditorFactory(),
_ => throw UnimplementedError(),
};
}
Expand Down
Loading

0 comments on commit f4ca3ef

Please sign in to comment.