mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add basic relation field (#4397)
* 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
This commit is contained in:
parent
f826d05f03
commit
f4ca3ef782
@ -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: []);
|
||||
}
|
@ -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: [],
|
||||
);
|
||||
}
|
@ -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,
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -144,9 +144,9 @@ class GridCreateFilterBloc
|
||||
fieldId: fieldId,
|
||||
condition: TextFilterConditionPB.Contains,
|
||||
);
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
return FlowyResult.success(null);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -9,6 +9,19 @@ 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});
|
||||
|
||||
@ -16,7 +29,7 @@ class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = FieldType.values.map((fieldType) {
|
||||
final cells = _supportedFieldTypes.map((fieldType) {
|
||||
return FieldTypeCell(
|
||||
fieldType: fieldType,
|
||||
onSelectField: (fieldType) {
|
||||
|
@ -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';
|
||||
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,160 @@
|
||||
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/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:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
import 'builder.dart';
|
||||
|
||||
class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
const RelationTypeOptionEditorFactory();
|
||||
|
||||
@override
|
||||
Widget? build({
|
||||
required BuildContext context,
|
||||
required String viewId,
|
||||
required FieldPB field,
|
||||
required PopoverMutex popoverMutex,
|
||||
required TypeOptionDataCallback onTypeOptionUpdated,
|
||||
}) {
|
||||
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,
|
||||
),
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
RelationTypeOptionPB _parseTypeOptionData(List<int> data) {
|
||||
return RelationTypeOptionDataParser().fromBuffer(data);
|
||||
}
|
||||
|
||||
RelationTypeOptionPB _updateTypeOption({
|
||||
required RelationTypeOptionPB typeOption,
|
||||
required String databaseId,
|
||||
}) {
|
||||
typeOption.freeze();
|
||||
return typeOption.rebuild((typeOption) {
|
||||
typeOption.databaseId = databaseId;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _DatabaseList extends StatefulWidget {
|
||||
const _DatabaseList({
|
||||
required this.onSelectDatabase,
|
||||
required this.currentDatabaseId,
|
||||
});
|
||||
|
||||
final String? currentDatabaseId;
|
||||
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
|
||||
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<List<DatabaseDescriptionPB>>((l) => l.items, (r) => [])
|
||||
.map((databaseDescription) {
|
||||
final databaseId = databaseDescription.databaseId;
|
||||
return FlowyButton(
|
||||
onTap: () => widget.onSelectDatabase(databaseId),
|
||||
text: FlowyText.medium(
|
||||
databaseId,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
rightIcon: databaseId == widget.currentDatabaseId
|
||||
? FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (_, __) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
itemCount: databaseIds.length,
|
||||
itemBuilder: (context, index) => databaseIds[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -84,6 +85,12 @@ class CardCellBuilder {
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
FieldType.Relation => RelationCardCell(
|
||||
key: key,
|
||||
style: isStyleOrNull(style),
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
_ => throw UnimplementedError,
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'card_cell.dart';
|
||||
|
||||
class RelationCardCellStyle extends CardCellStyle {
|
||||
RelationCardCellStyle({
|
||||
required super.padding,
|
||||
required this.textStyle,
|
||||
required this.wrap,
|
||||
});
|
||||
|
||||
final TextStyle textStyle;
|
||||
final bool wrap;
|
||||
}
|
||||
|
||||
class RelationCardCell extends CardCell<RelationCardCellStyle> {
|
||||
const RelationCardCell({
|
||||
super.key,
|
||||
required super.style,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
|
||||
@override
|
||||
State<RelationCardCell> createState() => _RelationCellState();
|
||||
}
|
||||
|
||||
class _RelationCellState extends State<RelationCardCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) {
|
||||
return RelationCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<RelationCellBloc, RelationCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.rows.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final children = state.rows
|
||||
.map(
|
||||
(row) => FlowyText.medium(
|
||||
row.name,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
padding: widget.style.padding,
|
||||
child: widget.style.wrap
|
||||
? Wrap(spacing: 4, runSpacing: 4, children: children)
|
||||
: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import '../card_cell_skeleton/checklist_card_cell.dart';
|
||||
import '../card_cell_skeleton/date_card_cell.dart';
|
||||
import '../card_cell_skeleton/number_card_cell.dart';
|
||||
import '../card_cell_skeleton/relation_card_cell.dart';
|
||||
import '../card_cell_skeleton/select_option_card_cell.dart';
|
||||
import '../card_cell_skeleton/text_card_cell.dart';
|
||||
import '../card_cell_skeleton/timestamp_card_cell.dart';
|
||||
@ -73,5 +74,10 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
FieldType.Relation: RelationCardCellStyle(
|
||||
padding: padding,
|
||||
wrap: true,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import '../card_cell_skeleton/checklist_card_cell.dart';
|
||||
import '../card_cell_skeleton/date_card_cell.dart';
|
||||
import '../card_cell_skeleton/number_card_cell.dart';
|
||||
import '../card_cell_skeleton/relation_card_cell.dart';
|
||||
import '../card_cell_skeleton/select_option_card_cell.dart';
|
||||
import '../card_cell_skeleton/text_card_cell.dart';
|
||||
import '../card_cell_skeleton/timestamp_card_cell.dart';
|
||||
@ -73,5 +74,10 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
FieldType.Relation: RelationCardCellStyle(
|
||||
padding: padding,
|
||||
wrap: true,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import '../card_cell_skeleton/checkbox_card_cell.dart';
|
||||
import '../card_cell_skeleton/checklist_card_cell.dart';
|
||||
import '../card_cell_skeleton/date_card_cell.dart';
|
||||
import '../card_cell_skeleton/number_card_cell.dart';
|
||||
import '../card_cell_skeleton/relation_card_cell.dart';
|
||||
import '../card_cell_skeleton/select_option_card_cell.dart';
|
||||
import '../card_cell_skeleton/text_card_cell.dart';
|
||||
import '../card_cell_skeleton/timestamp_card_cell.dart';
|
||||
@ -72,5 +73,10 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
FieldType.Relation: RelationCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
wrap: true,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/relation.dart';
|
||||
|
||||
class DesktopGridRelationCellSkin extends IEditableRelationCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
RelationCellBloc bloc,
|
||||
RelationCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 400),
|
||||
margin: EdgeInsets.zero,
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
popupBuilder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: RelationCellEditor(
|
||||
selectedRowIds: state.rows.map((row) => row.rowId).toList(),
|
||||
databaseId: state.relatedDatabaseId,
|
||||
onSelectRow: (rowId) {
|
||||
bloc.add(RelationCellEvent.selectRow(rowId));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: Wrap(
|
||||
runSpacing: 4.0,
|
||||
spacing: 4.0,
|
||||
children: state.rows
|
||||
.map(
|
||||
(row) => FlowyText.medium(
|
||||
row.name,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/relation.dart';
|
||||
|
||||
class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
RelationCellBloc bloc,
|
||||
RelationCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 400),
|
||||
margin: EdgeInsets.zero,
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
popupBuilder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<RelationCellBloc, RelationCellState>(
|
||||
builder: (context, state) => RelationCellEditor(
|
||||
selectedRowIds: state.rows.map((row) => row.rowId).toList(),
|
||||
databaseId: state.relatedDatabaseId,
|
||||
onSelectRow: (rowId) {
|
||||
context
|
||||
.read<RelationCellBloc>()
|
||||
.add(RelationCellEvent.selectRow(rowId));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import 'editable_cell_skeleton/checkbox.dart';
|
||||
import 'editable_cell_skeleton/checklist.dart';
|
||||
import 'editable_cell_skeleton/date.dart';
|
||||
import 'editable_cell_skeleton/number.dart';
|
||||
import 'editable_cell_skeleton/relation.dart';
|
||||
import 'editable_cell_skeleton/select_option.dart';
|
||||
import 'editable_cell_skeleton/text.dart';
|
||||
import 'editable_cell_skeleton/timestamp.dart';
|
||||
@ -106,6 +107,12 @@ class EditableCellBuilder {
|
||||
skin: IEditableURLCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
FieldType.Relation => EditableRelationCell(
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
skin: IEditableRelationCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
@ -186,6 +193,12 @@ class EditableCellBuilder {
|
||||
skin: skinMap.urlSkin!,
|
||||
key: key,
|
||||
),
|
||||
FieldType.Relation => EditableRelationCell(
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
skin: skinMap.relationSkin!,
|
||||
key: key,
|
||||
),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
@ -340,6 +353,7 @@ class EditableCellSkinMap {
|
||||
this.numberSkin,
|
||||
this.textSkin,
|
||||
this.urlSkin,
|
||||
this.relationSkin,
|
||||
});
|
||||
|
||||
final IEditableCheckboxCellSkin? checkboxSkin;
|
||||
@ -350,6 +364,7 @@ class EditableCellSkinMap {
|
||||
final IEditableNumberCellSkin? numberSkin;
|
||||
final IEditableTextCellSkin? textSkin;
|
||||
final IEditableURLCellSkin? urlSkin;
|
||||
final IEditableRelationCellSkin? relationSkin;
|
||||
|
||||
bool has(FieldType fieldType) {
|
||||
return switch (fieldType) {
|
||||
|
@ -0,0 +1,94 @@
|
||||
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/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../desktop_grid/desktop_grid_relation_cell.dart';
|
||||
import '../desktop_row_detail/desktop_row_detail_relation_cell.dart';
|
||||
import '../mobile_grid/mobile_grid_relation_cell.dart';
|
||||
import '../mobile_row_detail/mobile_row_detail_relation_cell.dart';
|
||||
|
||||
abstract class IEditableRelationCellSkin {
|
||||
factory IEditableRelationCellSkin.fromStyle(EditableCellStyle style) {
|
||||
return switch (style) {
|
||||
EditableCellStyle.desktopGrid => DesktopGridRelationCellSkin(),
|
||||
EditableCellStyle.desktopRowDetail => DesktopRowDetailRelationCellSkin(),
|
||||
EditableCellStyle.mobileGrid => MobileGridRelationCellSkin(),
|
||||
EditableCellStyle.mobileRowDetail => MobileRowDetailRelationCellSkin(),
|
||||
};
|
||||
}
|
||||
|
||||
const IEditableRelationCellSkin();
|
||||
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
RelationCellBloc bloc,
|
||||
RelationCellState state,
|
||||
PopoverController popoverController,
|
||||
);
|
||||
}
|
||||
|
||||
class EditableRelationCell extends EditableCellWidget {
|
||||
EditableRelationCell({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
required this.skin,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
final IEditableRelationCellSkin skin;
|
||||
|
||||
@override
|
||||
GridCellState<EditableRelationCell> createState() => _RelationCellState();
|
||||
}
|
||||
|
||||
class _RelationCellState extends GridCellState<EditableRelationCell> {
|
||||
final PopoverController _popover = PopoverController();
|
||||
late final cellBloc = RelationCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocBuilder<RelationCellBloc, RelationCellState>(
|
||||
builder: (context, state) {
|
||||
return widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
state,
|
||||
_popover,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequestFocus() {
|
||||
_popover.show();
|
||||
widget.cellContainerNotifier.isFocus = true;
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => "";
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../editable_cell_skeleton/relation.dart';
|
||||
|
||||
class MobileGridRelationCellSkin extends IEditableRelationCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
RelationCellBloc bloc,
|
||||
RelationCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
hoverColor: Colors.transparent,
|
||||
margin: EdgeInsets.zero,
|
||||
text: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: state.rows
|
||||
.map(
|
||||
(row) => FlowyText(
|
||||
row.name,
|
||||
fontSize: 15,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
builder: (context) {
|
||||
return const FlowyText("Coming soon");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileRowDetailRelationCellSkin extends IEditableRelationCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
RelationCellBloc bloc,
|
||||
RelationCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
padding: EdgeInsets.zero,
|
||||
builder: (context) {
|
||||
return const FlowyText("Coming soon");
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 48,
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
|
||||
child: Wrap(
|
||||
runSpacing: 4.0,
|
||||
spacing: 4.0,
|
||||
children: state.rows
|
||||
.map(
|
||||
(row) => FlowyText(
|
||||
row.name,
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.underline,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
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/widgets/common/type_option_separator.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 '../../application/cell/bloc/relation_cell_bloc.dart';
|
||||
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<String> 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 BlocProvider<RelationRowSearchBloc>(
|
||||
create: (context) => RelationRowSearchBloc(
|
||||
databaseId: databaseId,
|
||||
),
|
||||
child: BlocBuilder<RelationCellBloc, RelationCellState>(
|
||||
builder: (context, cellState) {
|
||||
return BlocBuilder<RelationRowSearchBloc, RelationRowSearchState>(
|
||||
builder: (context, state) {
|
||||
final children = state.filteredRows
|
||||
.map(
|
||||
(row) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
row.name.trim().isEmpty
|
||||
? LocaleKeys.grid_title_placeholder.tr()
|
||||
: row.name,
|
||||
color: row.name.trim().isEmpty
|
||||
? Theme.of(context).hintColor
|
||||
: null,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
rightIcon: cellState.rows
|
||||
.map((e) => e.rowId)
|
||||
.contains(row.rowId)
|
||||
? FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).primaryColor,
|
||||
)
|
||||
: null,
|
||||
onTap: () => onSelectRow(row.rowId),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(6.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0) +
|
||||
GridSize.typeOptionContentInsets,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.regular(
|
||||
LocaleKeys.grid_relation_inRelatedDatabase.tr(),
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(2.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
cellState.relatedDatabaseId,
|
||||
fontSize: 11,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: FlowyTextField(
|
||||
onChanged: (text) => context
|
||||
.read<RelationRowSearchBloc>()
|
||||
.add(RelationRowSearchEvent.updateFilter(text)),
|
||||
),
|
||||
),
|
||||
const VSpace(6.0),
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
if (state.filteredRows.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0) +
|
||||
GridSize.typeOptionContentInsets,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.grid_relation_emptySearchResult.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -75,6 +75,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
@override
|
||||
void dispose() {
|
||||
widget.textController.removeListener(_onChanged);
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.LastEditedTime =>
|
||||
LocaleKeys.grid_field_updatedAtFieldName.tr(),
|
||||
FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(),
|
||||
FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
|
||||
@ -34,6 +35,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.Checklist => FlowySvgs.checklist_s,
|
||||
FieldType.LastEditedTime => FlowySvgs.last_modified_s,
|
||||
FieldType.CreatedTime => FlowySvgs.created_at_s,
|
||||
FieldType.Relation => FlowySvgs.relation_s,
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
|
||||
@ -48,6 +50,7 @@ extension FieldTypeExtension on FieldType {
|
||||
FieldType.Checklist => const Color(0xFF98F4CD),
|
||||
FieldType.LastEditedTime => const Color(0xFFFDEDA7),
|
||||
FieldType.CreatedTime => const Color(0xFFFDEDA7),
|
||||
FieldType.Relation => const Color(0xFFFDEDA7),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="11.8274" cy="5.82739" r="1.5" stroke="#333333"/>
|
||||
<path d="M10.5008 5.38471L6.24097 4.78992" stroke="#333333"/>
|
||||
<path d="M4.86475 6.24121L6.02777 10.1009" stroke="#333333"/>
|
||||
<circle cx="7" cy="11" r="1.5" stroke="#333333"/>
|
||||
<circle cx="5" cy="5" r="1.5" stroke="#333333"/>
|
||||
<path d="M10.9011 7.14258L8.1484 10.0447" stroke="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 448 B |
@ -17,6 +17,7 @@ export const PropertyTypeText = ({ type }: { type: FieldType }) => {
|
||||
[FieldType.Checklist]: t('grid.field.checklistFieldName'),
|
||||
[FieldType.LastEditedTime]: t('grid.field.updatedAtFieldName'),
|
||||
[FieldType.CreatedTime]: t('grid.field.createdAtFieldName'),
|
||||
[FieldType.Relation]: t('grid.field.relationFieldName'),
|
||||
};
|
||||
|
||||
return map[type] || 'unknown';
|
||||
|
@ -9,6 +9,7 @@ import { ReactComponent as ChecklistSvg } from '$app/assets/database/field-type-
|
||||
import { ReactComponent as CheckboxSvg } from '$app/assets/database/field-type-checkbox.svg';
|
||||
import { ReactComponent as URLSvg } from '$app/assets/database/field-type-url.svg';
|
||||
import { ReactComponent as LastEditedTimeSvg } from '$app/assets/database/field-type-last-edited-time.svg';
|
||||
import { ReactComponent as RelationSvg } from '$app/assets/database/field-type-relation.svg';
|
||||
|
||||
export const FieldTypeSvgMap: Record<FieldType, FC<React.SVGProps<SVGSVGElement>>> = {
|
||||
[FieldType.RichText]: TextSvg,
|
||||
@ -21,6 +22,7 @@ export const FieldTypeSvgMap: Record<FieldType, FC<React.SVGProps<SVGSVGElement>
|
||||
[FieldType.Checklist]: ChecklistSvg,
|
||||
[FieldType.LastEditedTime]: LastEditedTimeSvg,
|
||||
[FieldType.CreatedTime]: LastEditedTimeSvg,
|
||||
[FieldType.Relation]: RelationSvg,
|
||||
};
|
||||
|
||||
export const ProppertyTypeSvg: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => {
|
||||
|
@ -588,6 +588,7 @@
|
||||
"multiSelectFieldName": "Multiselect",
|
||||
"urlFieldName": "URL",
|
||||
"checklistFieldName": "Checklist",
|
||||
"relationFieldName": "Relation",
|
||||
"numberFormat": "Number format",
|
||||
"dateFormat": "Date format",
|
||||
"includeTime": "Include time",
|
||||
@ -688,6 +689,12 @@
|
||||
"hideComplete": "Hide completed tasks",
|
||||
"showComplete": "Show all tasks"
|
||||
},
|
||||
"relation": {
|
||||
"relatedDatabasePlaceLabel": "Related Database",
|
||||
"relatedDatabasePlaceholder": "None",
|
||||
"inRelatedDatabase": "In",
|
||||
"emptySearchResult": "No records found"
|
||||
},
|
||||
"menuName": "Grid",
|
||||
"referencedGridPrefix": "View of",
|
||||
"calculate": "Calculate",
|
||||
|
@ -335,6 +335,16 @@ impl EventIntegrationTest {
|
||||
ChecklistCellDataPB::try_from(Bytes::from(cell.data)).unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_relation_cell(
|
||||
&self,
|
||||
view_id: &str,
|
||||
field_id: &str,
|
||||
row_id: &str,
|
||||
) -> RelationCellDataPB {
|
||||
let cell = self.get_cell(view_id, row_id, field_id).await;
|
||||
RelationCellDataPB::try_from(Bytes::from(cell.data)).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn update_checklist_cell(
|
||||
&self,
|
||||
changeset: ChecklistCellDataChangesetPB,
|
||||
@ -469,4 +479,33 @@ impl EventIntegrationTest {
|
||||
.parse::<RepeatedCalendarEventPB>()
|
||||
.items
|
||||
}
|
||||
|
||||
pub async fn update_relation_cell(
|
||||
&self,
|
||||
changeset: RelationCellChangesetPB,
|
||||
) -> Option<FlowyError> {
|
||||
EventBuilder::new(self.clone())
|
||||
.event(DatabaseEvent::UpdateRelationCell)
|
||||
.payload(changeset)
|
||||
.async_send()
|
||||
.await
|
||||
.error()
|
||||
}
|
||||
|
||||
pub async fn get_related_row_data(
|
||||
&self,
|
||||
database_id: String,
|
||||
row_ids: Vec<String>,
|
||||
) -> Vec<RelatedRowDataPB> {
|
||||
EventBuilder::new(self.clone())
|
||||
.event(DatabaseEvent::GetRelatedRowDatas)
|
||||
.payload(RepeatedRowIdPB {
|
||||
database_id,
|
||||
row_ids,
|
||||
})
|
||||
.async_send()
|
||||
.await
|
||||
.parse::<RepeatedRelatedRowDataPB>()
|
||||
.rows
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use event_integration::EventIntegrationTest;
|
||||
use flowy_database2::entities::{
|
||||
CellChangesetPB, CellIdPB, CheckboxCellDataPB, ChecklistCellDataChangesetPB, DatabaseLayoutPB,
|
||||
DatabaseSettingChangesetPB, DatabaseViewIdPB, DateCellChangesetPB, FieldType,
|
||||
OrderObjectPositionPB, SelectOptionCellDataPB, UpdateRowMetaChangesetPB,
|
||||
OrderObjectPositionPB, RelationCellChangesetPB, SelectOptionCellDataPB, UpdateRowMetaChangesetPB,
|
||||
};
|
||||
use lib_infra::util::timestamp;
|
||||
|
||||
@ -778,3 +778,99 @@ async fn create_calendar_event_test() {
|
||||
let events = test.get_all_calendar_events(&calendar_view.id).await;
|
||||
assert_eq!(events.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_relation_cell_test() {
|
||||
let test = EventIntegrationTest::new_with_guest_user().await;
|
||||
let current_workspace = test.get_current_workspace().await;
|
||||
let grid_view = test
|
||||
.create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![])
|
||||
.await;
|
||||
let relation_field = test.create_field(&grid_view.id, FieldType::Relation).await;
|
||||
let database = test.get_database(&grid_view.id).await;
|
||||
|
||||
// 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(),
|
||||
},
|
||||
inserted_row_ids: vec![
|
||||
"row1rowid".to_string(),
|
||||
"row2rowid".to_string(),
|
||||
"row3rowid".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(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_detailed_relation_cell_data() {
|
||||
let test = EventIntegrationTest::new_with_guest_user().await;
|
||||
let current_workspace = test.get_current_workspace().await;
|
||||
|
||||
let origin_grid_view = test
|
||||
.create_grid(¤t_workspace.id, "origin".to_owned(), vec![])
|
||||
.await;
|
||||
let relation_grid_view = test
|
||||
.create_grid(¤t_workspace.id, "relation grid".to_owned(), vec![])
|
||||
.await;
|
||||
let relation_field = test
|
||||
.create_field(&relation_grid_view.id, FieldType::Relation)
|
||||
.await;
|
||||
|
||||
let origin_database = test.get_database(&origin_grid_view.id).await;
|
||||
let origin_fields = test.get_all_database_fields(&origin_grid_view.id).await;
|
||||
let linked_row = origin_database.rows[0].clone();
|
||||
|
||||
test
|
||||
.update_cell(CellChangesetPB {
|
||||
view_id: origin_grid_view.id.clone(),
|
||||
row_id: linked_row.id.clone(),
|
||||
field_id: origin_fields.items[0].id.clone(),
|
||||
cell_changeset: "hello world".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
let new_database = test.get_database(&relation_grid_view.id).await;
|
||||
|
||||
// update the relation cell
|
||||
let changeset = RelationCellChangesetPB {
|
||||
view_id: relation_grid_view.id.clone(),
|
||||
cell_id: CellIdPB {
|
||||
view_id: relation_grid_view.id.clone(),
|
||||
field_id: relation_field.id.clone(),
|
||||
row_id: new_database.rows[0].id.clone(),
|
||||
},
|
||||
inserted_row_ids: vec![linked_row.id.clone()],
|
||||
..Default::default()
|
||||
};
|
||||
test.update_relation_cell(changeset).await;
|
||||
|
||||
// get the cell
|
||||
let cell = test
|
||||
.get_relation_cell(
|
||||
&relation_grid_view.id,
|
||||
&relation_field.id,
|
||||
&new_database.rows[0].id,
|
||||
)
|
||||
.await;
|
||||
|
||||
// using the row ids, get the row data
|
||||
let rows = test
|
||||
.get_related_row_data(origin_database.id.clone(), cell.row_ids)
|
||||
.await;
|
||||
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].name, "hello world");
|
||||
}
|
||||
|
@ -69,6 +69,12 @@ impl AsRef<str> for DatabaseIdPB {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug)]
|
||||
pub struct RepeatedDatabaseIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub value: Vec<DatabaseIdPB>,
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug, Validate)]
|
||||
pub struct DatabaseViewIdPB {
|
||||
#[pb(index = 1)]
|
||||
|
@ -473,6 +473,7 @@ pub enum FieldType {
|
||||
Checklist = 7,
|
||||
LastEditedTime = 8,
|
||||
CreatedTime = 9,
|
||||
Relation = 10,
|
||||
}
|
||||
|
||||
impl Display for FieldType {
|
||||
@ -509,8 +510,9 @@ impl FieldType {
|
||||
FieldType::Checkbox => "Checkbox",
|
||||
FieldType::URL => "URL",
|
||||
FieldType::Checklist => "Checklist",
|
||||
FieldType::LastEditedTime => "Last edited time",
|
||||
FieldType::LastEditedTime => "Last modified",
|
||||
FieldType::CreatedTime => "Created time",
|
||||
FieldType::Relation => "Relation",
|
||||
};
|
||||
s.to_string()
|
||||
}
|
||||
@ -559,6 +561,10 @@ impl FieldType {
|
||||
matches!(self, FieldType::Checklist)
|
||||
}
|
||||
|
||||
pub fn is_relation(&self) -> bool {
|
||||
matches!(self, FieldType::Relation)
|
||||
}
|
||||
|
||||
pub fn can_be_group(&self) -> bool {
|
||||
self.is_select_option() || self.is_checkbox() || self.is_url()
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ mod checklist_filter;
|
||||
mod date_filter;
|
||||
mod filter_changeset;
|
||||
mod number_filter;
|
||||
mod relation_filter;
|
||||
mod select_option_filter;
|
||||
mod text_filter;
|
||||
mod util;
|
||||
@ -12,6 +13,7 @@ pub use checklist_filter::*;
|
||||
pub use date_filter::*;
|
||||
pub use filter_changeset::*;
|
||||
pub use number_filter::*;
|
||||
pub use relation_filter::*;
|
||||
pub use select_option_filter::*;
|
||||
pub use text_filter::*;
|
||||
pub use util::*;
|
||||
|
@ -0,0 +1,24 @@
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
use crate::services::filter::{Filter, FromFilterString};
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||
pub struct RelationFilterPB {
|
||||
#[pb(index = 1)]
|
||||
pub condition: i64,
|
||||
}
|
||||
|
||||
impl FromFilterString for RelationFilterPB {
|
||||
fn from_filter(_filter: &Filter) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
RelationFilterPB { condition: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Filter> for RelationFilterPB {
|
||||
fn from(_filter: &Filter) -> Self {
|
||||
RelationFilterPB { condition: 0 }
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ use validator::Validate;
|
||||
use crate::entities::parser::NotEmptyStr;
|
||||
use crate::entities::{
|
||||
CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType,
|
||||
NumberFilterPB, SelectOptionFilterPB, TextFilterPB,
|
||||
NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
|
||||
};
|
||||
use crate::services::field::SelectOptionIds;
|
||||
use crate::services::filter::Filter;
|
||||
@ -44,6 +44,7 @@ impl std::convert::From<&Filter> for FilterPB {
|
||||
FieldType::Checklist => ChecklistFilterPB::from(filter).try_into().unwrap(),
|
||||
FieldType::Checkbox => CheckboxFilterPB::from(filter).try_into().unwrap(),
|
||||
FieldType::URL => TextFilterPB::from(filter).try_into().unwrap(),
|
||||
FieldType::Relation => RelationFilterPB::from(filter).try_into().unwrap(),
|
||||
};
|
||||
Self {
|
||||
id: filter.id.clone(),
|
||||
@ -186,6 +187,10 @@ impl TryInto<UpdateFilterParams> for UpdateFilterPayloadPB {
|
||||
condition = filter.condition as u8;
|
||||
content = SelectOptionIds::from(filter.option_ids).to_string();
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let filter = RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
|
||||
condition = filter.condition as u8;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(UpdateFilterParams {
|
||||
|
@ -14,8 +14,9 @@ macro_rules! impl_into_field_type {
|
||||
7 => FieldType::Checklist,
|
||||
8 => FieldType::LastEditedTime,
|
||||
9 => FieldType::CreatedTime,
|
||||
10 => FieldType::Relation,
|
||||
_ => {
|
||||
tracing::error!("🔴Can't parser FieldType from value: {}", ty);
|
||||
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
|
||||
FieldType::RichText
|
||||
},
|
||||
}
|
||||
@ -34,7 +35,7 @@ macro_rules! impl_into_field_visibility {
|
||||
1 => FieldVisibility::HideWhenEmpty,
|
||||
2 => FieldVisibility::AlwaysHidden,
|
||||
_ => {
|
||||
tracing::error!("🔴Can't parser FieldVisibility from value: {}", ty);
|
||||
tracing::error!("🔴Can't parse FieldVisibility from value: {}", ty);
|
||||
FieldVisibility::AlwaysShown
|
||||
},
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ mod checkbox_entities;
|
||||
mod checklist_entities;
|
||||
mod date_entities;
|
||||
mod number_entities;
|
||||
mod relation_entities;
|
||||
mod select_option_entities;
|
||||
mod text_entities;
|
||||
mod timestamp_entities;
|
||||
@ -11,6 +12,7 @@ pub use checkbox_entities::*;
|
||||
pub use checklist_entities::*;
|
||||
pub use date_entities::*;
|
||||
pub use number_entities::*;
|
||||
pub use relation_entities::*;
|
||||
pub use select_option_entities::*;
|
||||
pub use text_entities::*;
|
||||
pub use timestamp_entities::*;
|
||||
|
@ -0,0 +1,87 @@
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
use crate::entities::CellIdPB;
|
||||
use crate::services::field::{RelationCellData, RelationTypeOption};
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RelationCellDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub row_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<RelationCellData> for RelationCellDataPB {
|
||||
fn from(data: RelationCellData) -> Self {
|
||||
Self {
|
||||
row_ids: data.row_ids.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RelationCellDataPB> for RelationCellData {
|
||||
fn from(data: RelationCellDataPB) -> Self {
|
||||
Self {
|
||||
row_ids: data.row_ids.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RelationCellChangesetPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub cell_id: CellIdPB,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub inserted_row_ids: Vec<String>,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub removed_row_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct RelationTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
pub database_id: String,
|
||||
}
|
||||
|
||||
impl From<RelationTypeOption> for RelationTypeOptionPB {
|
||||
fn from(value: RelationTypeOption) -> Self {
|
||||
RelationTypeOptionPB {
|
||||
database_id: value.database_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RelationTypeOptionPB> for RelationTypeOption {
|
||||
fn from(value: RelationTypeOptionPB) -> Self {
|
||||
RelationTypeOption {
|
||||
database_id: value.database_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RelatedRowDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub row_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct RepeatedRelatedRowDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub rows: Vec<RelatedRowDataPB>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||
pub struct RepeatedRowIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub database_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub row_ids: Vec<String>,
|
||||
}
|
@ -13,7 +13,8 @@ use crate::entities::*;
|
||||
use crate::manager::DatabaseManager;
|
||||
use crate::services::cell::CellBuilder;
|
||||
use crate::services::field::{
|
||||
type_option_data_from_pb, ChecklistCellChangeset, DateCellChangeset, SelectOptionCellChangeset,
|
||||
type_option_data_from_pb, ChecklistCellChangeset, DateCellChangeset, RelationCellChangeset,
|
||||
SelectOptionCellChangeset,
|
||||
};
|
||||
use crate::services::field_settings::FieldSettingsChangesetParams;
|
||||
use crate::services::group::GroupChangeset;
|
||||
@ -978,3 +979,81 @@ pub(crate) async fn remove_calculation_handler(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_related_database_ids_handler(
|
||||
_data: AFPluginData<DatabaseViewIdPB>,
|
||||
_manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
pub(crate) async fn update_relation_cell_handler(
|
||||
data: AFPluginData<RelationCellChangesetPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let params: RelationCellChangesetPB = data.into_inner();
|
||||
let view_id = parser::NotEmptyStr::parse(params.view_id)
|
||||
.map_err(|_| flowy_error::ErrorCode::DatabaseViewIdIsEmpty)?
|
||||
.0;
|
||||
let cell_id: CellIdParams = params.cell_id.try_into()?;
|
||||
let params = RelationCellChangeset {
|
||||
inserted_row_ids: params
|
||||
.inserted_row_ids
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
removed_row_ids: params.removed_row_ids.into_iter().map(Into::into).collect(),
|
||||
};
|
||||
|
||||
let database_editor = manager.get_database_with_view_id(&view_id).await?;
|
||||
|
||||
// // get the related database
|
||||
// let related_database_id = database_editor
|
||||
// .get_related_database_id(&cell_id.field_id)
|
||||
// .await?;
|
||||
// let related_database_editor = manager.get_database(&related_database_id).await?;
|
||||
|
||||
// // validate the changeset contents
|
||||
// related_database_editor
|
||||
// .validate_row_ids_exist(¶ms)
|
||||
// .await?;
|
||||
|
||||
// update the cell in the database
|
||||
database_editor
|
||||
.update_cell_with_changeset(
|
||||
&view_id,
|
||||
cell_id.row_id,
|
||||
&cell_id.field_id,
|
||||
BoxAny::new(params),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_related_row_datas_handler(
|
||||
data: AFPluginData<RepeatedRowIdPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let params: RepeatedRowIdPB = data.into_inner();
|
||||
let database_editor = manager.get_database(¶ms.database_id).await?;
|
||||
let row_datas = database_editor
|
||||
.get_related_rows(Some(¶ms.row_ids))
|
||||
.await?;
|
||||
|
||||
data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas })
|
||||
}
|
||||
|
||||
pub(crate) async fn get_related_database_rows_handler(
|
||||
data: AFPluginData<DatabaseIdPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let database_id = data.into_inner().value;
|
||||
let database_editor = manager.get_database(&database_id).await?;
|
||||
let row_datas = database_editor.get_related_rows(None).await?;
|
||||
|
||||
data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas })
|
||||
}
|
||||
|
@ -83,6 +83,11 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler)
|
||||
.event(DatabaseEvent::UpdateCalculation, update_calculation_handler)
|
||||
.event(DatabaseEvent::RemoveCalculation, remove_calculation_handler)
|
||||
// Relation
|
||||
.event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler)
|
||||
.event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler)
|
||||
.event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler)
|
||||
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
|
||||
}
|
||||
|
||||
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
|
||||
@ -342,4 +347,22 @@ pub enum DatabaseEvent {
|
||||
|
||||
#[event(input = "RemoveCalculationChangesetPB")]
|
||||
RemoveCalculation = 165,
|
||||
|
||||
/// Currently unused. Get a list of database ids that this database relates
|
||||
/// to.
|
||||
#[event(input = "DatabaseViewIdPB", output = "RepeatedDatabaseIdPB")]
|
||||
GetRelatedDatabaseIds = 170,
|
||||
|
||||
/// Updates a relation cell, adding or removing links to rows in another
|
||||
/// database
|
||||
#[event(input = "RelationCellChangesetPB")]
|
||||
UpdateRelationCell = 171,
|
||||
|
||||
/// Get the names of the linked rows in a relation cell.
|
||||
#[event(input = "RepeatedRowIdPB", output = "RepeatedRelatedRowDataPB")]
|
||||
GetRelatedRowDatas = 172,
|
||||
|
||||
/// Get the names of all the rows in a related database.
|
||||
#[event(input = "DatabaseIdPB", output = "RepeatedRelatedRowDataPB")]
|
||||
GetRelatedDatabaseRows = 173,
|
||||
}
|
||||
|
@ -307,6 +307,9 @@ impl<'a> CellBuilder<'a> {
|
||||
cells.insert(field_id, insert_select_option_cell(ids.into_inner(), field));
|
||||
}
|
||||
},
|
||||
FieldType::Relation => {
|
||||
cells.insert(field_id, (&RelationCellData::from(cell_str)).into());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ use crate::services::database_view::{
|
||||
};
|
||||
use crate::services::field::{
|
||||
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
|
||||
type_option_data_from_pb, ChecklistCellChangeset, SelectOptionCellChangeset, SelectOptionIds,
|
||||
TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
|
||||
type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset,
|
||||
SelectOptionIds, StrCellData, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
|
||||
};
|
||||
use crate::services::field_settings::{
|
||||
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
|
||||
@ -1238,6 +1238,61 @@ impl DatabaseEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_related_database_id(&self, field_id: &str) -> FlowyResult<String> {
|
||||
let mut field = self
|
||||
.database
|
||||
.lock()
|
||||
.get_fields(Some(vec![field_id.to_string()]));
|
||||
let field = field.pop().ok_or(FlowyError::internal())?;
|
||||
|
||||
let type_option = field
|
||||
.get_type_option::<RelationTypeOption>(FieldType::Relation)
|
||||
.ok_or(FlowyError::record_not_found())?;
|
||||
|
||||
Ok(type_option.database_id)
|
||||
}
|
||||
|
||||
pub async fn get_related_rows(
|
||||
&self,
|
||||
row_ids: Option<&Vec<String>>,
|
||||
) -> FlowyResult<Vec<RelatedRowDataPB>> {
|
||||
let primary_field = self.database.lock().fields.get_primary_field().unwrap();
|
||||
let handler =
|
||||
TypeOptionCellExt::new_with_cell_data_cache(&primary_field, Some(self.cell_cache.clone()))
|
||||
.get_type_option_cell_data_handler(&FieldType::RichText)
|
||||
.ok_or(FlowyError::internal())?;
|
||||
|
||||
let row_data = {
|
||||
let database = self.database.lock();
|
||||
let mut rows = database.get_database_rows();
|
||||
if let Some(row_ids) = row_ids {
|
||||
rows.retain(|row| row_ids.contains(&row.id));
|
||||
}
|
||||
rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let title = database
|
||||
.get_cell(&primary_field.id, &row.id)
|
||||
.cell
|
||||
.and_then(|cell| {
|
||||
handler
|
||||
.get_cell_data(&cell, &FieldType::RichText, &primary_field)
|
||||
.ok()
|
||||
})
|
||||
.and_then(|cell_data| cell_data.unbox_or_none())
|
||||
.unwrap_or_else(|| StrCellData("".to_string()));
|
||||
|
||||
RelatedRowDataPB {
|
||||
row_id: row.id.to_string(),
|
||||
name: title.0,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Ok(row_data)
|
||||
}
|
||||
|
||||
fn get_auto_updated_fields(&self, view_id: &str) -> Vec<Field> {
|
||||
self
|
||||
.database
|
||||
|
@ -1,6 +1,6 @@
|
||||
mod field_builder;
|
||||
mod field_operation;
|
||||
mod type_options;
|
||||
pub mod type_options;
|
||||
|
||||
pub use field_builder::*;
|
||||
pub use field_operation::*;
|
||||
|
@ -2,6 +2,7 @@ pub mod checkbox_type_option;
|
||||
pub mod checklist_type_option;
|
||||
pub mod date_type_option;
|
||||
pub mod number_type_option;
|
||||
pub mod relation_type_option;
|
||||
pub mod selection_type_option;
|
||||
pub mod text_type_option;
|
||||
pub mod timestamp_type_option;
|
||||
@ -14,6 +15,7 @@ pub use checkbox_type_option::*;
|
||||
pub use checklist_type_option::*;
|
||||
pub use date_type_option::*;
|
||||
pub use number_type_option::*;
|
||||
pub use relation_type_option::*;
|
||||
pub use selection_type_option::*;
|
||||
pub use text_type_option::*;
|
||||
pub use timestamp_type_option::*;
|
||||
|
@ -0,0 +1,5 @@
|
||||
mod relation;
|
||||
mod relation_entities;
|
||||
|
||||
pub use relation::*;
|
||||
pub use relation_entities::*;
|
@ -0,0 +1,159 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
|
||||
use collab_database::rows::Cell;
|
||||
use flowy_error::FlowyResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::{FieldType, RelationCellDataPB, RelationFilterPB};
|
||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||
use crate::services::field::{
|
||||
default_order, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
||||
TypeOptionCellDataSerde, TypeOptionTransform,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
|
||||
use super::{RelationCellChangeset, RelationCellData};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct RelationTypeOption {
|
||||
pub database_id: String,
|
||||
}
|
||||
|
||||
impl From<TypeOptionData> for RelationTypeOption {
|
||||
fn from(value: TypeOptionData) -> Self {
|
||||
let database_id = value.get_str_value("database_id").unwrap_or_default();
|
||||
Self { database_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RelationTypeOption> for TypeOptionData {
|
||||
fn from(value: RelationTypeOption) -> Self {
|
||||
TypeOptionDataBuilder::new()
|
||||
.insert_str_value("database_id", value.database_id)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOption for RelationTypeOption {
|
||||
type CellData = RelationCellData;
|
||||
type CellChangeset = RelationCellChangeset;
|
||||
type CellProtobufType = RelationCellDataPB;
|
||||
type CellFilter = RelationFilterPB;
|
||||
}
|
||||
|
||||
impl CellDataChangeset for RelationTypeOption {
|
||||
fn apply_changeset(
|
||||
&self,
|
||||
changeset: RelationCellChangeset,
|
||||
cell: Option<Cell>,
|
||||
) -> FlowyResult<(Cell, RelationCellData)> {
|
||||
if cell.is_none() {
|
||||
let cell_data = RelationCellData {
|
||||
row_ids: changeset.inserted_row_ids,
|
||||
};
|
||||
|
||||
return Ok(((&cell_data).into(), cell_data));
|
||||
}
|
||||
|
||||
let cell_data: RelationCellData = cell.unwrap().as_ref().into();
|
||||
let mut row_ids = cell_data.row_ids.clone();
|
||||
for inserted in changeset.inserted_row_ids.iter() {
|
||||
if row_ids.iter().any(|row_id| row_id == inserted) {
|
||||
row_ids.push(inserted.clone())
|
||||
}
|
||||
}
|
||||
for removed_id in changeset.removed_row_ids.iter() {
|
||||
if let Some(index) = row_ids.iter().position(|row_id| row_id == removed_id) {
|
||||
row_ids.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
let cell_data = RelationCellData { row_ids };
|
||||
|
||||
Ok(((&cell_data).into(), cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataDecoder for RelationTypeOption {
|
||||
fn decode_cell(
|
||||
&self,
|
||||
cell: &Cell,
|
||||
decoded_field_type: &FieldType,
|
||||
_field: &Field,
|
||||
) -> FlowyResult<RelationCellData> {
|
||||
if !decoded_field_type.is_relation() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
Ok(cell.into())
|
||||
}
|
||||
|
||||
fn stringify_cell_data(&self, cell_data: RelationCellData) -> String {
|
||||
cell_data.to_string()
|
||||
}
|
||||
|
||||
fn stringify_cell(&self, cell: &Cell) -> String {
|
||||
let cell_data = RelationCellData::from(cell);
|
||||
cell_data.to_string()
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataCompare for RelationTypeOption {
|
||||
fn apply_cmp(
|
||||
&self,
|
||||
_cell_data: &RelationCellData,
|
||||
_other_cell_data: &RelationCellData,
|
||||
_sort_condition: SortCondition,
|
||||
) -> Ordering {
|
||||
default_order()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataFilter for RelationTypeOption {
|
||||
fn apply_filter(
|
||||
&self,
|
||||
_filter: &RelationFilterPB,
|
||||
_field_type: &FieldType,
|
||||
_cell_data: &RelationCellData,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionTransform for RelationTypeOption {
|
||||
fn transformable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn transform_type_option(
|
||||
&mut self,
|
||||
_old_type_option_field_type: FieldType,
|
||||
_old_type_option_data: TypeOptionData,
|
||||
) {
|
||||
}
|
||||
|
||||
fn transform_type_option_cell(
|
||||
&self,
|
||||
_cell: &Cell,
|
||||
_transformed_field_type: &FieldType,
|
||||
_field: &Field,
|
||||
) -> Option<RelationCellData> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataSerde for RelationTypeOption {
|
||||
fn protobuf_encode(&self, cell_data: RelationCellData) -> RelationCellDataPB {
|
||||
cell_data.into()
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<RelationCellData> {
|
||||
Ok(cell.into())
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collab::preclude::Any;
|
||||
use collab_database::rows::{new_cell_builder, Cell, RowId};
|
||||
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::{TypeOptionCellData, CELL_DATA};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RelationCellData {
|
||||
pub row_ids: Vec<RowId>,
|
||||
}
|
||||
|
||||
impl From<&Cell> for RelationCellData {
|
||||
fn from(value: &Cell) -> Self {
|
||||
let row_ids = match value.get(CELL_DATA) {
|
||||
Some(Any::Array(array)) => array
|
||||
.iter()
|
||||
.flat_map(|item| {
|
||||
if let Any::String(string) = item {
|
||||
Some(RowId::from(string.clone().to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
Self { row_ids }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RelationCellData> for Cell {
|
||||
fn from(value: &RelationCellData) -> Self {
|
||||
let data = Any::Array(Arc::from(
|
||||
value
|
||||
.row_ids
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|id| Any::String(Arc::from(id.to_string())))
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
new_cell_builder(FieldType::Relation)
|
||||
.insert_any(CELL_DATA, data)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for RelationCellData {
|
||||
fn from(s: String) -> Self {
|
||||
if s.is_empty() {
|
||||
return RelationCellData { row_ids: vec![] };
|
||||
}
|
||||
|
||||
let ids = s
|
||||
.split(", ")
|
||||
.map(|id| id.to_string().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
RelationCellData { row_ids: ids }
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellData for RelationCellData {
|
||||
fn is_cell_empty(&self) -> bool {
|
||||
self.row_ids.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for RelationCellData {
|
||||
fn to_string(&self) -> String {
|
||||
self
|
||||
.row_ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RelationCellChangeset {
|
||||
pub inserted_row_ids: Vec<RowId>,
|
||||
pub removed_row_ids: Vec<RowId>,
|
||||
}
|
@ -10,14 +10,14 @@ use flowy_error::FlowyResult;
|
||||
|
||||
use crate::entities::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
|
||||
TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
};
|
||||
use crate::services::cell::CellDataDecoder;
|
||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
|
||||
SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
};
|
||||
use crate::services::filter::FromFilterString;
|
||||
use crate::services::sort::SortCondition;
|
||||
@ -202,6 +202,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
|
||||
FieldType::Checklist => {
|
||||
ChecklistTypeOptionPB::try_from(bytes).map(|pb| ChecklistTypeOption::from(pb).into())
|
||||
},
|
||||
FieldType::Relation => {
|
||||
RelationTypeOptionPB::try_from(bytes).map(|pb| RelationTypeOption::from(pb).into())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,6 +260,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let relation_type_option: RelationTypeOption = type_option.into();
|
||||
RelationTypeOptionPB::from(relation_type_option)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,5 +285,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
|
||||
FieldType::Checkbox => CheckboxTypeOption::default().into(),
|
||||
FieldType::URL => URLTypeOption::default().into(),
|
||||
FieldType::Checklist => ChecklistTypeOption.into(),
|
||||
FieldType::Relation => RelationTypeOption::default().into(),
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,10 @@ use crate::services::cell::{
|
||||
};
|
||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
|
||||
SingleSelectTypeOption, TimestampTypeOption, TypeOption, TypeOptionCellDataCompare,
|
||||
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
|
||||
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
|
||||
TypeOptionTransform, URLTypeOption,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
|
||||
@ -490,6 +491,16 @@ impl<'a> TypeOptionCellExt<'a> {
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
FieldType::Relation => self
|
||||
.field
|
||||
.get_type_option::<RelationTypeOption>(field_type)
|
||||
.map(|type_option| {
|
||||
TypeOptionCellDataHandlerImpl::new_with_boxed(
|
||||
type_option,
|
||||
self.cell_filter_cache.clone(),
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -568,6 +579,9 @@ fn get_type_option_transform_handler(
|
||||
FieldType::Checklist => {
|
||||
Box::new(ChecklistTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
FieldType::Relation => {
|
||||
Box::new(RelationTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,6 +358,12 @@ impl FilterController {
|
||||
.write()
|
||||
.insert(field_id, ChecklistFilterPB::from_filter(filter.as_ref()));
|
||||
},
|
||||
FieldType::Relation => {
|
||||
self
|
||||
.cell_filter_cache
|
||||
.write()
|
||||
.insert(field_id, RelationFilterPB::from_filter(filter.as_ref()));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ use std::time::Duration;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::{
|
||||
ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption,
|
||||
SelectOptionCellChangeset, SingleSelectTypeOption, StrCellData, URLCellData,
|
||||
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StrCellData,
|
||||
URLCellData,
|
||||
};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
|
||||
@ -52,6 +53,10 @@ async fn grid_cell_update() {
|
||||
}),
|
||||
FieldType::Checkbox => BoxAny::new("1".to_string()),
|
||||
FieldType::URL => BoxAny::new("1".to_string()),
|
||||
FieldType::Relation => BoxAny::new(RelationCellChangeset {
|
||||
inserted_row_ids: vec!["abcdefabcdef".to_string().into()],
|
||||
..Default::default()
|
||||
}),
|
||||
_ => BoxAny::new("".to_string()),
|
||||
};
|
||||
|
||||
|
@ -5,8 +5,8 @@ use strum::IntoEnumIterator;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use flowy_database2::services::field::{
|
||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor,
|
||||
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, RelationTypeOption,
|
||||
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
};
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
use flowy_database2::services::setting::BoardLayoutSetting;
|
||||
@ -126,6 +126,16 @@ pub fn make_test_board() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(checklist_field);
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let type_option = RelationTypeOption {
|
||||
database_id: "".to_string(),
|
||||
};
|
||||
let relation_field = FieldBuilder::new(field_type, type_option)
|
||||
.name("Related")
|
||||
.visibility(true)
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,7 +237,6 @@ pub fn make_test_board() -> DatabaseData {
|
||||
FieldType::SingleSelect => {
|
||||
row_builder.insert_single_select_cell(|mut options| options.remove(2))
|
||||
},
|
||||
|
||||
FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
|
||||
_ => "".to_owned(),
|
||||
};
|
||||
|
@ -3,10 +3,10 @@ use collab_database::views::{DatabaseLayout, DatabaseView};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use flowy_database2::services::field::{
|
||||
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption,
|
||||
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
|
||||
NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor,
|
||||
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
|
||||
};
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
|
||||
@ -128,6 +128,16 @@ pub fn make_test_grid() -> DatabaseData {
|
||||
.build();
|
||||
fields.push(checklist_field);
|
||||
},
|
||||
FieldType::Relation => {
|
||||
let type_option = RelationTypeOption {
|
||||
database_id: "".to_string(),
|
||||
};
|
||||
let relation_field = FieldBuilder::new(field_type, type_option)
|
||||
.name("Related")
|
||||
.visibility(true)
|
||||
.build();
|
||||
fields.push(relation_field);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,14 +27,14 @@ async fn export_csv_test() {
|
||||
let test = DatabaseEditorTest::new_grid().await;
|
||||
let database = test.editor.clone();
|
||||
let s = database.export_csv(CSVFormat::Original).await.unwrap();
|
||||
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At
|
||||
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,First thing,,
|
||||
,$2,2022/03/14,,"Google,Twitter",Yes,,"Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed",,
|
||||
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,,
|
||||
DA,$14,2022/11/17,Completed,,No,,Task 1,,
|
||||
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,,
|
||||
AE,$5,2022/12/25,Planned,Facebook,Yes,,"Sprint,Sprint some more,Rest",,
|
||||
CB,,,,,,,,,
|
||||
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At,Related
|
||||
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,First thing,,,
|
||||
,$2,2022/03/14,,"Google,Twitter",Yes,,"Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed",,,
|
||||
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,,,
|
||||
DA,$14,2022/11/17,Completed,,No,,Task 1,,,
|
||||
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,,,
|
||||
AE,$5,2022/12/25,Planned,Facebook,Yes,,"Sprint,Sprint some more,Rest",,,
|
||||
CB,,,,,,,,,,
|
||||
"#;
|
||||
println!("{}", s);
|
||||
assert_eq!(s, expected);
|
||||
@ -99,6 +99,7 @@ async fn export_and_then_import_meta_csv_test() {
|
||||
FieldType::Checklist => {},
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
@ -180,6 +181,7 @@ async fn history_database_import_test() {
|
||||
FieldType::Checklist => {},
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
|
Loading…
Reference in New Issue
Block a user